@pnpm/installing.commands 1100.8.0 → 1100.10.0

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/add.js CHANGED
@@ -49,6 +49,8 @@ export function rcOptionsTypes() {
49
49
  'lockfile',
50
50
  'modules-dir',
51
51
  'network-concurrency',
52
+ 'node-experimental-package-map',
53
+ 'node-package-map-type',
52
54
  'node-linker',
53
55
  'noproxy',
54
56
  'npm-path',
@@ -275,21 +277,26 @@ export async function handler(opts, params, commands) {
275
277
  for (const pkg of opts.allowBuild) {
276
278
  mergedAllowBuilds[pkg] = true;
277
279
  }
278
- return installDeps({
280
+ await installDeps({
279
281
  ...opts,
280
282
  allowBuilds: mergedAllowBuilds,
281
283
  rebuildHandler: commands?.rebuild,
282
284
  fetchFullMetadata: getFetchFullMetadata(opts),
283
285
  include,
284
286
  includeDirect: include,
287
+ // `--dry-run` is an `install`-only preview; never let a config-level
288
+ // `dry-run` turn `add` into a no-op check.
289
+ dryRun: false,
285
290
  }, params);
291
+ return;
286
292
  }
287
- return installDeps({
293
+ await installDeps({
288
294
  ...opts,
289
295
  rebuildHandler: commands?.rebuild,
290
296
  fetchFullMetadata: getFetchFullMetadata(opts),
291
297
  include,
292
298
  includeDirect: include,
299
+ dryRun: false,
293
300
  }, params);
294
301
  }
295
302
  //# sourceMappingURL=add.js.map
package/lib/dedupe.js CHANGED
@@ -49,13 +49,15 @@ export async function handler(opts, _params, commands) {
49
49
  devDependencies: opts.dev !== false,
50
50
  optionalDependencies: opts.optional !== false,
51
51
  };
52
- return installDeps({
52
+ await installDeps({
53
53
  ...opts,
54
54
  rebuildHandler: commands?.rebuild,
55
55
  dedupe: true,
56
56
  include,
57
57
  includeDirect: include,
58
58
  lockfileCheck: opts.check ? dedupeDiffCheck : undefined,
59
+ // `--dry-run` is an `install`-only preview; `dedupe` has its own `--check`.
60
+ dryRun: false,
59
61
  }, []);
60
62
  }
61
63
  //# sourceMappingURL=dedupe.js.map
@@ -12,8 +12,8 @@ import { findWorkspaceProjects } from '@pnpm/workspace.projects-reader';
12
12
  import { sequenceGraph } from '@pnpm/workspace.projects-sorter';
13
13
  import * as structUtils from '@yarnpkg/core/structUtils';
14
14
  import { parse as parseYarnLockfile } from '@yarnpkg/lockfile';
15
- import { parseSyml } from '@yarnpkg/parsers';
16
15
  import { rimraf } from '@zkochan/rimraf';
16
+ import yaml from 'js-yaml';
17
17
  import { loadJsonFile } from 'load-json-file';
18
18
  import { map as mapValues } from 'ramda';
19
19
  import { renderHelp } from 'render-help';
@@ -135,7 +135,7 @@ async function readYarnLockFile(dir) {
135
135
  throw new PnpmError('YARN_LOCKFILE_NOT_FOUND', 'No yarn.lock found');
136
136
  }
137
137
  function parseYarn2Lock(lockFileContents) {
138
- const parseYarnLock = parseSyml(lockFileContents);
138
+ const parseYarnLock = parseYarn2Yaml(lockFileContents);
139
139
  delete parseYarnLock.__metadata;
140
140
  const dependencies = {};
141
141
  const { parseDescriptor, parseRange } = structUtils;
@@ -151,6 +151,18 @@ function parseYarn2Lock(lockFileContents) {
151
151
  type: YarnLockType.yarn2,
152
152
  };
153
153
  }
154
+ function parseYarn2Yaml(lockFileContents) {
155
+ const parseYarnLock = yaml.load(lockFileContents, {
156
+ schema: yaml.FAILSAFE_SCHEMA,
157
+ json: true,
158
+ });
159
+ if (parseYarnLock == null)
160
+ return {};
161
+ if (typeof parseYarnLock !== 'object' || Array.isArray(parseYarnLock)) {
162
+ throw new PnpmError('YARN_LOCKFILE_PARSE_FAILED', `Expected an indexed object, got ${Array.isArray(parseYarnLock) ? 'an array' : `a ${typeof parseYarnLock}`} instead. Does your file follow YAML's rules?`);
163
+ }
164
+ return parseYarnLock;
165
+ }
154
166
  async function readNpmLockfile(dir) {
155
167
  try {
156
168
  return await loadJsonFile(path.join(dir, 'package-lock.json'));
@@ -230,7 +242,7 @@ function selectProjectByDir(projects, searchedDir) {
230
242
  const project = projects.find(({ rootDir }) => path.relative(rootDir, searchedDir) === '');
231
243
  if (project == null)
232
244
  return undefined;
233
- return { [searchedDir]: { dependencies: [], package: project } };
245
+ return { [project.rootDir]: { dependencies: [], package: project } };
234
246
  }
235
247
  function getYarnLockfileType(lockFileContents) {
236
248
  return lockFileContents.includes('__metadata')
package/lib/install.d.ts CHANGED
@@ -7,7 +7,7 @@ export declare const shorthands: Record<string, string>;
7
7
  export declare const commandNames: string[];
8
8
  export declare const recursiveByDefault = true;
9
9
  export declare function help(): string;
10
- export type InstallCommandOptions = Pick<Config, 'autoInstallPeers' | 'bail' | 'bin' | 'catalogs' | 'configDependencies' | 'dedupeInjectedDeps' | 'dedupeDirectDeps' | 'dedupePeerDependents' | 'dedupePeers' | 'deployAllFiles' | 'depth' | 'dev' | 'enableGlobalVirtualStore' | 'engineStrict' | 'excludeLinksFromLockfile' | 'frozenLockfile' | 'global' | 'globalPnpmfile' | 'hoistPattern' | 'hoistingLimits' | 'publicHoistPattern' | 'ignorePnpmfile' | 'ignoreScripts' | 'injectWorkspacePackages' | 'linkWorkspacePackages' | 'lockfileDir' | 'lockfileOnly' | 'optimisticRepeatInstall' | 'modulesDir' | 'nodeLinker' | 'patchedDependencies' | 'preferFrozenLockfile' | 'preferWorkspacePackages' | 'production' | 'registries' | 'save' | 'saveDev' | 'saveExact' | 'saveOptional' | 'savePeer' | 'savePrefix' | 'saveProd' | 'saveCatalogName' | 'saveWorkspaceProtocol' | 'lockfileIncludeTarballUrl' | 'sideEffectsCache' | 'sideEffectsCacheReadonly' | 'sort' | 'sharedWorkspaceLockfile' | 'tag' | 'trustLockfile' | 'allowBuilds' | 'optional' | 'virtualStoreDir' | 'workspaceConcurrency' | 'workspaceDir' | 'workspacePackagePatterns' | 'extraEnv' | 'resolutionMode' | 'ignoreWorkspaceCycles' | 'disallowWorkspaceCycles' | 'updateConfig' | 'overrides' | 'packageExtensions' | 'pnprServer' | 'supportedArchitectures' | 'packageConfigs'> & Pick<ConfigContext, 'allProjects' | 'cliOptions' | 'hooks' | 'rootProjectManifest' | 'rootProjectManifestDir' | 'allProjectsGraph' | 'selectedProjectsGraph'> & CreateStoreControllerOptions & Partial<Pick<Config, 'globalPkgDir'>> & {
10
+ export type InstallCommandOptions = Pick<Config, 'autoInstallPeers' | 'bail' | 'bin' | 'catalogs' | 'configDependencies' | 'dedupeInjectedDeps' | 'dedupeDirectDeps' | 'dedupePeerDependents' | 'dedupePeers' | 'deployAllFiles' | 'depth' | 'dev' | 'dryRun' | 'enableGlobalVirtualStore' | 'engineStrict' | 'excludeLinksFromLockfile' | 'frozenLockfile' | 'global' | 'globalPnpmfile' | 'hoistPattern' | 'hoistingLimits' | 'publicHoistPattern' | 'ignorePnpmfile' | 'ignoreScripts' | 'injectWorkspacePackages' | 'linkWorkspacePackages' | 'lockfileDir' | 'lockfileOnly' | 'optimisticRepeatInstall' | 'modulesDir' | 'nodeLinker' | 'patchedDependencies' | 'preferFrozenLockfile' | 'preferWorkspacePackages' | 'production' | 'registries' | 'save' | 'saveDev' | 'saveExact' | 'saveOptional' | 'savePeer' | 'savePrefix' | 'saveProd' | 'saveCatalogName' | 'saveWorkspaceProtocol' | 'lockfileIncludeTarballUrl' | 'sideEffectsCache' | 'sideEffectsCacheReadonly' | 'sort' | 'sharedWorkspaceLockfile' | 'tag' | 'trustLockfile' | 'allowBuilds' | 'optional' | 'virtualStoreDir' | 'workspaceConcurrency' | 'workspaceDir' | 'workspacePackagePatterns' | 'extraEnv' | 'resolutionMode' | 'ignoreWorkspaceCycles' | 'disallowWorkspaceCycles' | 'updateConfig' | 'overrides' | 'packageExtensions' | 'pnprServer' | 'supportedArchitectures' | 'packageConfigs'> & Pick<ConfigContext, 'allProjects' | 'cliOptions' | 'hooks' | 'rootProjectManifest' | 'rootProjectManifestDir' | 'allProjectsGraph' | 'selectedProjectsGraph'> & CreateStoreControllerOptions & Partial<Pick<Config, 'globalPkgDir'>> & {
11
11
  argv: {
12
12
  cooked?: string[];
13
13
  original: string[];
@@ -30,4 +30,4 @@ export type InstallCommandOptions = Pick<Config, 'autoInstallPeers' | 'bail' | '
30
30
  } & Partial<Pick<Config, 'ci' | 'modulesCacheMaxAge' | 'pnpmHomeDir' | 'preferWorkspacePackages' | 'strictDepBuilds' | 'useLockfile' | 'symlink'>>;
31
31
  export declare function handler(opts: InstallCommandOptions & {
32
32
  _calledFromLink?: boolean;
33
- }, _params?: string[], commands?: CommandHandlerMap): Promise<void>;
33
+ }, _params?: string[], commands?: CommandHandlerMap): Promise<void | string>;
package/lib/install.js CHANGED
@@ -3,6 +3,8 @@ import { docsUrl } from '@pnpm/cli.utils';
3
3
  import { types as allTypes } from '@pnpm/config.reader';
4
4
  import { WANTED_LOCKFILE } from '@pnpm/constants';
5
5
  import { PnpmError } from '@pnpm/error';
6
+ import { calcDedupeCheckIssues, countDedupeCheckIssues } from '@pnpm/installing.dedupe.check';
7
+ import { renderDedupeCheckIssues } from '@pnpm/installing.dedupe.issues-renderer';
6
8
  import { pick } from 'ramda';
7
9
  import { renderHelp } from 'render-help';
8
10
  import { getFetchFullMetadata } from './getFetchFullMetadata.js';
@@ -41,6 +43,8 @@ export function rcOptionsTypes() {
41
43
  'merge-git-branch-lockfiles-branch-pattern',
42
44
  'modules-dir',
43
45
  'network-concurrency',
46
+ 'node-experimental-package-map',
47
+ 'node-package-map-type',
44
48
  'node-linker',
45
49
  'noproxy',
46
50
  'package-import-method',
@@ -71,6 +75,7 @@ export function rcOptionsTypes() {
71
75
  'optional',
72
76
  'unsafe-perm',
73
77
  'verify-store-integrity',
78
+ 'frozen-store',
74
79
  'virtual-store-dir',
75
80
  'virtual-store-only',
76
81
  ], allTypes);
@@ -78,6 +83,7 @@ export function rcOptionsTypes() {
78
83
  export const cliOptionsTypes = () => ({
79
84
  ...rcOptionsTypes(),
80
85
  ...pick(['force'], allTypes),
86
+ 'dry-run': Boolean,
81
87
  'fix-lockfile': Boolean,
82
88
  'update-checksums': Boolean,
83
89
  'resolution-only': Boolean,
@@ -127,6 +133,10 @@ For options that may be used with `-r`, see "pnpm help recursive"',
127
133
  description: 'Skip reinstall if the workspace state is up-to-date',
128
134
  name: '--optimistic-repeat-install',
129
135
  },
136
+ {
137
+ description: 'Report what an install would change without writing anything to disk (no lockfile, no node_modules). Resolution still runs against the registry.',
138
+ name: '--dry-run',
139
+ },
130
140
  {
131
141
  description: '`optionalDependencies` are not installed',
132
142
  name: '--no-optional',
@@ -211,6 +221,10 @@ by any dependencies, so it is an emulation of a flat node_modules',
211
221
  description: 'If false, skips store integrity checks. These checks detect accidental corruption, not tampering by untrusted users with write access to the store',
212
222
  name: '--[no-]verify-store-integrity',
213
223
  },
224
+ {
225
+ description: 'Open the package store read-only (immutable) and skip all store writes. For installs against a store on a read-only filesystem (e.g. a Nix store); pair with --offline --frozen-lockfile. Incompatible with --force',
226
+ name: '--frozen-store',
227
+ },
214
228
  {
215
229
  description: 'Fail on missing or invalid peer dependencies',
216
230
  name: '--strict-peer-dependencies',
@@ -265,6 +279,18 @@ Install all optionalDependencies even when they don\'t satisfy the current envir
265
279
  description: 'Re-runs resolution: useful for printing out peer dependency issues',
266
280
  name: '--resolution-only',
267
281
  },
282
+ {
283
+ description: 'Override CPU architecture of native modules to install. Acceptable values are the same as the `cpu` field of `package.json` (from `process.arch`)',
284
+ name: '--cpu <arch>',
285
+ },
286
+ {
287
+ description: 'Override OS of native modules to install. Acceptable values are the same as the `os` field of `package.json` (from `process.platform`)',
288
+ name: '--os <os>',
289
+ },
290
+ {
291
+ description: 'Override libc of native modules to install. Acceptable values are the same as the `libc` field of `package.json`',
292
+ name: '--libc <libc>',
293
+ },
268
294
  ...UNIVERSAL_OPTIONS,
269
295
  ],
270
296
  },
@@ -299,6 +325,48 @@ export async function handler(opts, _params, commands) {
299
325
  installDepsOptions.lockfileOnly = true;
300
326
  installDepsOptions.forceFullResolution = true;
301
327
  }
302
- return installDeps(installDepsOptions, []);
328
+ if (opts.dryRun) {
329
+ return dryRunInstall(installDepsOptions, opts);
330
+ }
331
+ await installDeps(installDepsOptions, []);
332
+ }
333
+ /**
334
+ * Runs a full resolution but writes nothing to disk (no lockfile, no
335
+ * `node_modules`), then reports what a real install would change. Exits
336
+ * successfully regardless of whether changes were found — mirroring the
337
+ * preview semantics of `npm install --dry-run`.
338
+ */
339
+ async function dryRunInstall(installDepsOptions, opts) {
340
+ if (opts.pnprServer) {
341
+ throw new PnpmError('CONFIG_CONFLICT_DRY_RUN_WITH_PNPR_SERVER', 'Cannot use --dry-run with a configured pnpr server because the pnpr install path resolves and links through the server');
342
+ }
343
+ // `dryRun` makes the installer resolve fully and return the before/after
344
+ // wanted lockfile without writing anything. `lockfileOnly` keeps it from
345
+ // materializing `node_modules` and skips the metadata cache (resolution
346
+ // skips fetching). The optimistic fast path is disabled so resolution
347
+ // always runs.
348
+ installDepsOptions.optimisticRepeatInstall = false;
349
+ installDepsOptions.lockfileOnly = true;
350
+ installDepsOptions.dryRun = true;
351
+ const dryRunResult = await installDeps(installDepsOptions, []);
352
+ if (dryRunResult == null) {
353
+ // No comparison was produced — this install configuration's resolve path
354
+ // doesn't surface the dry-run lockfiles (e.g. a workspace without a
355
+ // shared lockfile). Report that explicitly instead of claiming "up to
356
+ // date", but keep `--dry-run`'s exit-0 contract.
357
+ return 'Dry run complete. Could not compute the changes for this install configuration (no shared lockfile to compare).';
358
+ }
359
+ return renderDryRunReport(dryRunResult);
360
+ }
361
+ function renderDryRunReport(dryRunResult) {
362
+ const issues = calcDedupeCheckIssues(dryRunResult.originalLockfile, dryRunResult.wantedLockfile, { includeImporterSpecifiers: true });
363
+ if (countDedupeCheckIssues(issues) === 0) {
364
+ return `Dry run complete. ${WANTED_LOCKFILE} is up to date; a real install would make no changes.`;
365
+ }
366
+ return [
367
+ 'Dry run complete. A real install would make the following changes (nothing was written to disk):',
368
+ '',
369
+ renderDedupeCheckIssues(issues),
370
+ ].join('\n');
303
371
  }
304
372
  //# sourceMappingURL=install.js.map
@@ -1,10 +1,10 @@
1
1
  import type { CommandHandler } from '@pnpm/cli.command';
2
2
  import type { Config, ConfigContext } from '@pnpm/config.reader';
3
- import { type UpdateMatchingFunction } from '@pnpm/installing.deps-installer';
3
+ import { type DryRunInstallResult, type UpdateMatchingFunction } from '@pnpm/installing.deps-installer';
4
4
  import type { LockfileObject } from '@pnpm/lockfile.types';
5
5
  import { type CreateStoreControllerOptions } from '@pnpm/store.connection-manager';
6
6
  import type { IncludedDependencies, PackageVulnerabilityAudit } from '@pnpm/types';
7
- export type InstallDepsOptions = Pick<Config, 'autoInstallPeers' | 'bail' | 'bin' | 'catalogs' | 'catalogMode' | 'cleanupUnusedCatalogs' | 'dedupePeerDependents' | 'dedupePeers' | 'depth' | 'dev' | 'enableGlobalVirtualStore' | 'virtualStoreOnly' | 'engineStrict' | 'excludeLinksFromLockfile' | 'global' | 'globalPnpmfile' | 'ignoreCurrentSpecifiers' | 'ignorePnpmfile' | 'ignoreScripts' | 'optimisticRepeatInstall' | 'linkWorkspacePackages' | 'lockfileDir' | 'lockfileOnly' | 'pnprServer' | 'production' | 'preferWorkspacePackages' | 'registries' | 'runtime' | 'runtimeOnFail' | 'save' | 'saveDev' | 'saveExact' | 'saveOptional' | 'savePeer' | 'savePrefix' | 'saveProd' | 'saveWorkspaceProtocol' | 'lockfileIncludeTarballUrl' | 'scriptsPrependNodePath' | 'scriptShell' | 'sideEffectsCache' | 'sideEffectsCacheReadonly' | 'sort' | 'sharedWorkspaceLockfile' | 'shellEmulator' | 'tag' | 'trustLockfile' | 'allowBuilds' | 'optional' | 'workspaceConcurrency' | 'workspaceDir' | 'workspacePackagePatterns' | 'extraEnv' | 'ignoreWorkspaceCycles' | 'disallowWorkspaceCycles' | 'configDependencies' | 'packageExtensions' | 'updateConfig'> & Pick<ConfigContext, 'allProjects' | 'allProjectsGraph' | 'cliOptions' | 'hooks' | 'rootProjectManifestDir' | 'rootProjectManifest' | 'selectedProjectsGraph'> & Partial<Pick<Config, 'ci'>> & CreateStoreControllerOptions & {
7
+ export type InstallDepsOptions = Pick<Config, 'autoInstallPeers' | 'bail' | 'bin' | 'catalogs' | 'catalogMode' | 'cleanupUnusedCatalogs' | 'dedupePeerDependents' | 'dedupePeers' | 'depth' | 'dev' | 'enableGlobalVirtualStore' | 'virtualStoreOnly' | 'engineStrict' | 'excludeLinksFromLockfile' | 'global' | 'globalPnpmfile' | 'ignoreCurrentSpecifiers' | 'ignorePnpmfile' | 'ignoreScripts' | 'optimisticRepeatInstall' | 'linkWorkspacePackages' | 'lockfileDir' | 'lockfileOnly' | 'pnprServer' | 'production' | 'preferWorkspacePackages' | 'registries' | 'runtime' | 'runtimeOnFail' | 'save' | 'saveDev' | 'saveExact' | 'saveOptional' | 'savePeer' | 'savePrefix' | 'saveProd' | 'saveWorkspaceProtocol' | 'lockfileIncludeTarballUrl' | 'scriptsPrependNodePath' | 'scriptShell' | 'sideEffectsCache' | 'sideEffectsCacheReadonly' | 'sort' | 'sharedWorkspaceLockfile' | 'shellEmulator' | 'tag' | 'trustLockfile' | 'allowBuilds' | 'optional' | 'workspaceConcurrency' | 'workspaceDir' | 'workspacePackagePatterns' | 'extraEnv' | 'ignoreWorkspaceCycles' | 'disallowWorkspaceCycles' | 'configDependencies' | 'packageExtensions' | 'updateConfig' | 'virtualStoreDirMaxLength'> & Pick<ConfigContext, 'allProjects' | 'allProjectsGraph' | 'cliOptions' | 'hooks' | 'rootProjectManifestDir' | 'rootProjectManifest' | 'selectedProjectsGraph'> & Partial<Pick<Config, 'ci'>> & CreateStoreControllerOptions & {
8
8
  argv: {
9
9
  cooked?: string[];
10
10
  original: string[];
@@ -48,5 +48,5 @@ export type InstallDepsOptions = Pick<Config, 'autoInstallPeers' | 'bail' | 'bin
48
48
  * subcommand — see `runPacquet.ts`'s `noRuntime` opt.
49
49
  */
50
50
  isInstallCommand?: boolean;
51
- } & Partial<Pick<Config, 'pnpmHomeDir' | 'strictDepBuilds' | 'useLockfile' | 'useGitBranchLockfile'>>;
52
- export declare function installDeps(opts: InstallDepsOptions, params: string[]): Promise<void>;
51
+ } & Partial<Pick<Config, 'dryRun' | 'pnpmHomeDir' | 'strictDepBuilds' | 'useLockfile' | 'useGitBranchLockfile' | 'mergeGitBranchLockfiles'>>;
52
+ export declare function installDeps(opts: InstallDepsOptions, params: string[]): Promise<DryRunInstallResult | undefined>;
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { buildProjects } from '@pnpm/building.after-install';
3
+ import { mergeCatalogs } from '@pnpm/catalogs.config';
3
4
  import { readProjectManifestOnly, tryReadProjectManifest, } from '@pnpm/cli.utils';
4
5
  import { checkDepsStatus } from '@pnpm/deps.status';
5
6
  import { PnpmError } from '@pnpm/error';
@@ -32,6 +33,7 @@ export async function installDeps(opts, params) {
32
33
  const { upToDate, wantedLockfileToRestore } = await checkDepsStatus({
33
34
  ...opts,
34
35
  ignoreFilteredInstallCache: true,
36
+ treatLocalFileDepsAsOutdated: true,
35
37
  });
36
38
  if (upToDate && await restoreWantedLockfileIfMissing(wantedLockfileToRestore, opts)) {
37
39
  if (opts.hooks?.customResolvers?.some(r => r.shouldRefreshResolution)) {
@@ -95,6 +97,7 @@ export async function installDeps(opts, params) {
95
97
  packageName: pacquetConfigDepName,
96
98
  argv: { original: opts.argv.original, remain: opts.argv.remain ?? [] },
97
99
  isInstallCommand: opts.isInstallCommand === true,
100
+ virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
98
101
  })
99
102
  : undefined;
100
103
  const includeDirect = opts.includeDirect ?? {
@@ -130,7 +133,7 @@ export async function installDeps(opts, params) {
130
133
  const allProjectsGraph = opts.allProjectsGraph ?? createProjectsGraph(allProjects, {
131
134
  linkWorkspacePackages: Boolean(opts.linkWorkspacePackages),
132
135
  }).graph;
133
- await recursiveInstallThenUpdateWorkspaceState(allProjects, params, {
136
+ return recursiveInstallThenUpdateWorkspaceState(allProjects, params, {
134
137
  ...opts,
135
138
  preferredVersions: opts.packageVulnerabilityAudit ? preferNonvulnerablePackageVersions(opts.packageVulnerabilityAudit) : undefined,
136
139
  allProjectsGraph,
@@ -139,7 +142,6 @@ export async function installDeps(opts, params) {
139
142
  workspaceDir: opts.workspaceDir,
140
143
  runPacquet,
141
144
  }, opts.update ? 'update' : (params.length === 0 ? 'install' : 'add'));
142
- return;
143
145
  }
144
146
  }
145
147
  // `pnpm install ""` is going to be just `pnpm install`
@@ -240,8 +242,8 @@ export async function installDeps(opts, params) {
240
242
  rootDir: opts.dir,
241
243
  targetDependenciesField: getSaveType(opts),
242
244
  };
243
- const { updatedCatalogs, updatedProject, ignoredBuilds, resolutionPolicyViolations } = await mutateModulesInSingleProject(mutatedProject, installOpts);
244
- if (opts.save !== false) {
245
+ const { updatedCatalogs, updatedProject, ignoredBuilds, resolutionPolicyViolations, dryRunResult } = await mutateModulesInSingleProject(mutatedProject, installOpts);
246
+ if (opts.save !== false && !opts.dryRun) {
245
247
  // Only pick entries when we'll actually persist. Otherwise the
246
248
  // info log would claim we added entries the workspace manifest
247
249
  // never saw, and the next install would re-prompt or fail
@@ -260,7 +262,7 @@ export async function installDeps(opts, params) {
260
262
  if (!opts.lockfileOnly) {
261
263
  await updateWorkspaceState({
262
264
  allProjects,
263
- settings: opts,
265
+ settings: withUpdatedCatalogs(opts, updatedCatalogs),
264
266
  workspaceDir: opts.workspaceDir ?? opts.lockfileDir ?? opts.dir,
265
267
  pnpmfiles: opts.pnpmfile,
266
268
  filteredInstall: allProjects.length !== Object.keys(opts.selectedProjectsGraph ?? {}).length,
@@ -268,9 +270,9 @@ export async function installDeps(opts, params) {
268
270
  });
269
271
  }
270
272
  await handleIgnoredBuilds(opts, ignoredBuilds);
271
- return;
273
+ return dryRunResult;
272
274
  }
273
- const { updatedCatalogs, updatedManifest, ignoredBuilds, resolutionPolicyViolations } = await install(manifest, {
275
+ const { updatedCatalogs, updatedManifest, ignoredBuilds, resolutionPolicyViolations, dryRunResult } = await install(manifest, {
274
276
  ...installOpts,
275
277
  updatePackageManifest,
276
278
  updateMatching,
@@ -279,7 +281,7 @@ export async function installDeps(opts, params) {
279
281
  // from this install" — both package.json and the workspace manifest.
280
282
  // Skip the pick so the info log doesn't claim entries were added that
281
283
  // were never written; the next install will resurface them.
282
- if (opts.save !== false) {
284
+ if (opts.save !== false && !opts.dryRun) {
283
285
  const policyUpdates = policyHandlers?.pickManifestUpdates(resolutionPolicyViolations);
284
286
  if (opts.update === true) {
285
287
  await Promise.all([
@@ -318,7 +320,7 @@ export async function installDeps(opts, params) {
318
320
  selectedProjectsGraph,
319
321
  workspaceDir: opts.workspaceDir, // Otherwise TypeScript doesn't understand that is not undefined
320
322
  runPacquet,
321
- }, 'install');
323
+ }, 'install', updatedCatalogs);
322
324
  if (opts.ignoreScripts)
323
325
  return;
324
326
  await buildProjects([
@@ -339,7 +341,7 @@ export async function installDeps(opts, params) {
339
341
  if (!opts.lockfileOnly) {
340
342
  await updateWorkspaceState({
341
343
  allProjects,
342
- settings: opts,
344
+ settings: withUpdatedCatalogs(opts, updatedCatalogs),
343
345
  workspaceDir: opts.workspaceDir ?? opts.lockfileDir ?? opts.dir,
344
346
  pnpmfiles: opts.pnpmfile,
345
347
  filteredInstall: allProjects.length !== Object.keys(opts.selectedProjectsGraph ?? {}).length,
@@ -347,26 +349,39 @@ export async function installDeps(opts, params) {
347
349
  });
348
350
  }
349
351
  }
352
+ return dryRunResult;
350
353
  }
351
354
  function selectProjectByDir(projects, searchedDir) {
352
355
  const project = projects.find(({ rootDir }) => path.relative(rootDir, searchedDir) === '');
353
356
  if (project == null)
354
357
  return undefined;
355
- return { [searchedDir]: { dependencies: [], package: project } };
358
+ return { [project.rootDir]: { dependencies: [], package: project } };
356
359
  }
357
- async function recursiveInstallThenUpdateWorkspaceState(allProjects, params, opts, cmdFullName) {
360
+ async function recursiveInstallThenUpdateWorkspaceState(allProjects, params, opts, cmdFullName, updatedCatalogs) {
358
361
  const recursiveResult = await recursive(allProjects, params, opts, cmdFullName);
359
362
  if (!opts.lockfileOnly) {
360
363
  await updateWorkspaceState({
361
364
  allProjects,
362
- settings: opts,
365
+ settings: withUpdatedCatalogs(opts, updatedCatalogs, recursiveResult.updatedCatalogs),
363
366
  workspaceDir: opts.workspaceDir,
364
367
  pnpmfiles: opts.pnpmfile,
365
368
  filteredInstall: allProjects.length !== Object.keys(opts.selectedProjectsGraph ?? {}).length,
366
369
  configDependencies: opts.configDependencies,
367
370
  });
368
371
  }
369
- return recursiveResult;
372
+ return recursiveResult.dryRunResult;
373
+ }
374
+ /**
375
+ * Folds the catalog entries written to `pnpm-workspace.yaml` during this
376
+ * install into the catalogs read at startup. The workspace state cache records
377
+ * these so a later install detects when a catalog entry was reverted; without
378
+ * this, the cache would keep the stale pre-install catalogs and report
379
+ * "Already up to date" even though the manifest changed.
380
+ */
381
+ function withUpdatedCatalogs(settings, ...updatedCatalogs) {
382
+ if (updatedCatalogs.every((catalogs) => catalogs == null))
383
+ return settings;
384
+ return { ...settings, catalogs: mergeCatalogs(settings.catalogs, ...updatedCatalogs) };
370
385
  }
371
386
  function severityStringToNumber(severity) {
372
387
  switch (severity) {
package/lib/link.js CHANGED
@@ -31,6 +31,7 @@ export function cliOptionsTypes() {
31
31
  'save-exact',
32
32
  'save-optional',
33
33
  'save-prefix',
34
+ 'trust-lockfile',
34
35
  'unsafe-perm',
35
36
  ], allTypes);
36
37
  }
package/lib/prune.js CHANGED
@@ -39,7 +39,7 @@ export function help() {
39
39
  });
40
40
  }
41
41
  export async function handler(opts) {
42
- return install.handler({
42
+ await install.handler({
43
43
  ...opts,
44
44
  modulesCacheMaxAge: 0,
45
45
  pruneDirectDependencies: true,
@@ -1,12 +1,13 @@
1
+ import type { Catalogs } from '@pnpm/catalogs.types';
1
2
  import type { CommandHandler } from '@pnpm/cli.command';
2
3
  import { type Config, type ConfigContext } from '@pnpm/config.reader';
3
- import { type UpdateMatchingFunction } from '@pnpm/installing.deps-installer';
4
+ import { type DryRunInstallResult, type UpdateMatchingFunction } from '@pnpm/installing.deps-installer';
4
5
  import type { PreferredVersions } from '@pnpm/resolving.resolver-base';
5
6
  import type { ResolutionVerifier } from '@pnpm/resolving.resolver-base';
6
7
  import { type CreateStoreControllerOptions } from '@pnpm/store.connection-manager';
7
8
  import type { StoreController } from '@pnpm/store.controller';
8
9
  import type { IncludedDependencies, Project, ProjectManifest, ProjectsGraph } from '@pnpm/types';
9
- export type RecursiveOptions = CreateStoreControllerOptions & Pick<Config, 'bail' | 'configDependencies' | 'dedupePeerDependents' | 'dedupePeers' | 'depth' | 'globalPnpmfile' | 'hoistPattern' | 'hoistingLimits' | 'ignorePnpmfile' | 'ignoreScripts' | 'linkWorkspacePackages' | 'lockfileDir' | 'lockfileOnly' | 'modulesDir' | 'pnprServer' | 'allowBuilds' | 'registries' | 'runtime' | 'save' | 'saveCatalogName' | 'saveDev' | 'saveExact' | 'saveOptional' | 'savePeer' | 'savePrefix' | 'saveProd' | 'saveWorkspaceProtocol' | 'lockfileIncludeTarballUrl' | 'sharedWorkspaceLockfile' | 'tag' | 'trustLockfile' | 'cleanupUnusedCatalogs' | 'packageConfigs' | 'updateConfig'> & Pick<ConfigContext, 'hooks' | 'rootProjectManifest' | 'rootProjectManifestDir'> & {
10
+ export type RecursiveOptions = CreateStoreControllerOptions & Pick<Config, 'bail' | 'configDependencies' | 'dedupePeerDependents' | 'dedupePeers' | 'depth' | 'dryRun' | 'globalPnpmfile' | 'hoistPattern' | 'hoistingLimits' | 'ignorePnpmfile' | 'ignoreScripts' | 'linkWorkspacePackages' | 'lockfileDir' | 'lockfileOnly' | 'modulesDir' | 'pnprServer' | 'allowBuilds' | 'registries' | 'runtime' | 'save' | 'saveCatalogName' | 'saveDev' | 'saveExact' | 'saveOptional' | 'savePeer' | 'savePrefix' | 'saveProd' | 'saveWorkspaceProtocol' | 'lockfileIncludeTarballUrl' | 'sharedWorkspaceLockfile' | 'tag' | 'trustLockfile' | 'cleanupUnusedCatalogs' | 'packageConfigs' | 'updateConfig'> & Pick<ConfigContext, 'hooks' | 'rootProjectManifest' | 'rootProjectManifestDir'> & {
10
11
  rebuildHandler?: CommandHandler;
11
12
  include?: IncludedDependencies;
12
13
  includeDirect?: IncludedDependencies;
@@ -32,14 +33,34 @@ export type RecursiveOptions = CreateStoreControllerOptions & Pick<Config, 'bail
32
33
  pnpmfile: string[];
33
34
  /**
34
35
  * Alternative install engine (today: pacquet) the deps-installer
35
- * delegates the materialization phase to. Built in `installDeps`
36
- * when `configDependencies.pacquet` is declared, threaded through
37
- * here so the recursive workspace path picks it up too.
36
+ * delegates the install to. Built in `installDeps` when
37
+ * `configDependencies.pacquet` is declared, threaded through here so
38
+ * the recursive workspace path picks it up too.
38
39
  */
39
- runPacquet?: () => Promise<void>;
40
+ runPacquet?: {
41
+ supportsResolution: boolean;
42
+ run: (opts?: {
43
+ filterResolvedProgress?: boolean;
44
+ resolve?: boolean;
45
+ }) => Promise<void>;
46
+ };
40
47
  } & Partial<Pick<Config, 'ci' | 'sort' | 'strictDepBuilds' | 'workspaceConcurrency'>> & Required<Pick<Config, 'workspaceDir'>>;
41
48
  export type CommandFullName = 'install' | 'add' | 'remove' | 'update' | 'import';
42
- export declare function recursive(allProjects: Project[], params: string[], opts: RecursiveOptions, cmdFullName: CommandFullName): Promise<boolean | string>;
49
+ export interface RecursiveResult {
50
+ passed: boolean | string;
51
+ /**
52
+ * Catalog entries written to `pnpm-workspace.yaml` during this install.
53
+ * The caller folds these into the catalogs recorded in the workspace state
54
+ * cache so that reverting a catalog entry is detected as an outdated state.
55
+ */
56
+ updatedCatalogs?: Catalogs;
57
+ /**
58
+ * Present only for a `dryRun` install over a shared workspace lockfile:
59
+ * the before/after wanted lockfiles for the caller to diff.
60
+ */
61
+ dryRunResult?: DryRunInstallResult;
62
+ }
63
+ export declare function recursive(allProjects: Project[], params: string[], opts: RecursiveOptions, cmdFullName: CommandFullName): Promise<RecursiveResult>;
43
64
  export declare function matchDependencies(match: (input: string) => string | null, manifest: ProjectManifest, include: IncludedDependencies): string[];
44
65
  export type UpdateDepsMatcher = (input: string) => string | null;
45
66
  export declare function createMatcher(params: string[]): UpdateDepsMatcher;
package/lib/recursive.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { mergeCatalogs } from '@pnpm/catalogs.config';
3
4
  import { throwOnCommandFail, } from '@pnpm/cli.utils';
4
5
  import { createMatcherWithIndex } from '@pnpm/config.matcher';
5
6
  import { createProjectConfigRecord, getWorkspaceConcurrency, } from '@pnpm/config.reader';
@@ -23,11 +24,11 @@ import { createWorkspaceSpecs, updateToWorkspacePackagesFromManifest } from './u
23
24
  export async function recursive(allProjects, params, opts, cmdFullName) {
24
25
  if (allProjects.length === 0) {
25
26
  // It might make sense to throw an exception in this case
26
- return false;
27
+ return { passed: false };
27
28
  }
28
29
  const pkgs = Object.values(opts.selectedProjectsGraph).map((wsPkg) => wsPkg.package);
29
30
  if (pkgs.length === 0) {
30
- return false;
31
+ return { passed: false };
31
32
  }
32
33
  const manifestsByPath = getManifestsByPath(allProjects);
33
34
  const throwOnFail = throwOnCommandFail.bind(null, `pnpm recursive ${cmdFullName}`);
@@ -88,7 +89,7 @@ export async function recursive(allProjects, params, opts, cmdFullName) {
88
89
  const isFromWorkspace = isSubdir.bind(null, calculatedRepositoryRoot);
89
90
  importers = await pFilter(importers, async ({ rootDirRealPath }) => isFromWorkspace(rootDirRealPath));
90
91
  if (importers.length === 0)
91
- return true;
92
+ return { passed: true };
92
93
  let mutation;
93
94
  switch (cmdFullName) {
94
95
  case 'remove':
@@ -176,12 +177,12 @@ export async function recursive(allProjects, params, opts, cmdFullName) {
176
177
  if ((mutatedImporters.length === 0) && cmdFullName === 'update' && opts.depth === 0) {
177
178
  throw new PnpmError('NO_PACKAGE_IN_DEPENDENCIES', 'None of the specified packages were found in the dependencies of any of the projects.');
178
179
  }
179
- const { updatedCatalogs, updatedProjects: mutatedPkgs, ignoredBuilds, resolutionPolicyViolations, } = await mutateModules(mutatedImporters, {
180
+ const { updatedCatalogs, updatedProjects: mutatedPkgs, ignoredBuilds, resolutionPolicyViolations, dryRunResult, } = await mutateModules(mutatedImporters, {
180
181
  ...installOpts,
181
182
  storeController: store.ctrl,
182
183
  resolutionVerifiers: store.resolutionVerifiers,
183
184
  });
184
- if (opts.save !== false) {
185
+ if (opts.save !== false && !opts.dryRun) {
185
186
  // Only pick entries when we'll actually persist. Otherwise the
186
187
  // info log would claim entries were added that the workspace
187
188
  // manifest never saw, and the next install would re-prompt or
@@ -199,7 +200,7 @@ export async function recursive(allProjects, params, opts, cmdFullName) {
199
200
  await Promise.all(promises);
200
201
  }
201
202
  await handleIgnoredBuilds(opts, ignoredBuilds);
202
- return true;
203
+ return { passed: true, updatedCatalogs, dryRunResult };
203
204
  }
204
205
  const pkgPaths = Object.keys(opts.selectedProjectsGraph).sort();
205
206
  let updatedCatalogs;
@@ -289,8 +290,10 @@ export async function recursive(allProjects, params, opts, cmdFullName) {
289
290
  if (opts.save !== false) {
290
291
  await writeProjectManifest(newManifest);
291
292
  if (newCatalogsAddition) {
292
- updatedCatalogs ??= {};
293
- Object.assign(updatedCatalogs, newCatalogsAddition);
293
+ // Per-project additions are partial maps keyed by catalog name then
294
+ // dependency. Merge at the dependency level so two projects updating
295
+ // different entries of the same catalog don't clobber each other.
296
+ updatedCatalogs = mergeCatalogs(updatedCatalogs, newCatalogsAddition);
294
297
  }
295
298
  }
296
299
  if (ignoredBuilds?.size) {
@@ -346,7 +349,7 @@ export async function recursive(allProjects, params, opts, cmdFullName) {
346
349
  if (!Object.values(result).filter(({ status }) => status === 'passed').length && cmdFullName === 'update' && opts.depth === 0) {
347
350
  throw new PnpmError('NO_PACKAGE_IN_DEPENDENCIES', 'None of the specified packages were found in the dependencies of any of the projects.');
348
351
  }
349
- return true;
352
+ return { passed: true, updatedCatalogs };
350
353
  }
351
354
  function calculateRepositoryRoot(workspaceDir, projectDirs) {
352
355
  // assume repo root is workspace dir
package/lib/remove.js CHANGED
@@ -39,6 +39,8 @@ export function rcOptionsTypes() {
39
39
  'lockfile-dir',
40
40
  'lockfile-only',
41
41
  'lockfile',
42
+ 'node-experimental-package-map',
43
+ 'node-package-map-type',
42
44
  'node-linker',
43
45
  'package-import-method',
44
46
  'pnpmfile',
@@ -137,6 +139,9 @@ export async function handler(opts, params) {
137
139
  storeDir: store.dir,
138
140
  resolutionVerifiers: store.resolutionVerifiers,
139
141
  include,
142
+ // `--dry-run` is an `install`-only preview; never let a config-level
143
+ // `dry-run` turn `remove` into a no-op check.
144
+ dryRun: false,
140
145
  });
141
146
  const allProjects = opts.allProjects ?? (opts.workspaceDir
142
147
  ? await findWorkspaceProjects(opts.workspaceDir, { ...opts, patterns: opts.workspacePackagePatterns })
@@ -1,5 +1,12 @@
1
1
  export interface MakeRunPacquetOpts {
2
2
  lockfileDir: string;
3
+ /**
4
+ * Effective pnpm config value. Forwarded through `PNPM_CONFIG_*` so
5
+ * pacquet writes the same `.modules.yaml` and virtual-store paths as
6
+ * the pnpm process that delegated to it, including Windows' shorter
7
+ * default.
8
+ */
9
+ virtualStoreDirMaxLength: number;
3
10
  /**
4
11
  * Which `configDependencies` entry installed pacquet: either the
5
12
  * original unscoped `pacquet` or the official scoped
@@ -32,21 +39,6 @@ export interface MakeRunPacquetOpts {
32
39
  */
33
40
  isInstallCommand: boolean;
34
41
  }
35
- /**
36
- * Build the install-engine callback `mutateModules` invokes when
37
- * `configDependencies` declares pacquet.
38
- *
39
- * The callback spawns the pacquet binary installed under
40
- * `node_modules/.pnpm-config/pacquet`. From `pnpm install`/`pnpm i` it
41
- * forwards the user's own pnpm CLI flags to pacquet's `install`
42
- * subcommand; from `add`/`update`/`dedupe` it doesn't forward (warning
43
- * instead). Pacquet's NDJSON stderr is parsed line-by-line and the
44
- * valid JSON records are re-emitted on pnpm's global `streamParser` so
45
- * `@pnpm/cli.default-reporter` renders pacquet's events the same way it
46
- * renders pnpm's own. Non-JSON stderr lines (panic backtraces,
47
- * unexpected diagnostics) are forwarded to the real stderr verbatim so
48
- * they reach the user.
49
- */
50
42
  /** Args the deps-installer passes per pacquet invocation. */
51
43
  export interface RunPacquetCallOpts {
52
44
  /**
@@ -60,5 +52,51 @@ export interface RunPacquetCallOpts {
60
52
  * source.
61
53
  */
62
54
  filterResolvedProgress?: boolean;
55
+ /**
56
+ * `true` to let pacquet perform the resolution itself rather than
57
+ * materialize an already-resolved lockfile. Drops the injected
58
+ * `--frozen-lockfile`, so pacquet resolves the manifests, writes
59
+ * `pnpm-lock.yaml`, and materializes in a single pass. Only valid
60
+ * when {@link PacquetEngine.supportsResolution} is `true` (pacquet
61
+ * >= 0.11.7).
62
+ */
63
+ resolve?: boolean;
63
64
  }
64
- export declare function makeRunPacquet(opts: MakeRunPacquetOpts): (callOpts?: RunPacquetCallOpts) => Promise<void>;
65
+ /**
66
+ * Handle to the pacquet install engine: its capabilities plus the
67
+ * callback `mutateModules` invokes to run it.
68
+ */
69
+ export interface PacquetEngine {
70
+ /**
71
+ * `true` when the installed pacquet is new enough (>= 0.11.7) to
72
+ * perform dependency resolution itself. When `false`, pacquet can
73
+ * only materialize an already-resolved lockfile, so the deps-installer
74
+ * runs its own resolve pass first and hands the written lockfile to
75
+ * pacquet.
76
+ */
77
+ supportsResolution: boolean;
78
+ run: (callOpts?: RunPacquetCallOpts) => Promise<void>;
79
+ }
80
+ /**
81
+ * Build the pacquet install engine `mutateModules` delegates to when
82
+ * `configDependencies` declares pacquet.
83
+ *
84
+ * `run` spawns the pacquet binary installed under
85
+ * `node_modules/.pnpm-config/pacquet`. From `pnpm install`/`pnpm i` it
86
+ * forwards the user's own pnpm CLI flags to pacquet's `install`
87
+ * subcommand; from `add`/`update`/`dedupe` it doesn't forward (warning
88
+ * instead). Pacquet's NDJSON stderr is parsed line-by-line and the
89
+ * valid JSON records are re-emitted on pnpm's global `streamParser` so
90
+ * `@pnpm/cli.default-reporter` renders pacquet's events the same way it
91
+ * renders pnpm's own. Non-JSON stderr lines (panic backtraces,
92
+ * unexpected diagnostics) are forwarded to the real stderr verbatim so
93
+ * they reach the user.
94
+ */
95
+ export declare function makeRunPacquet(opts: MakeRunPacquetOpts): PacquetEngine;
96
+ /**
97
+ * Name of the `@pacquet/<platform>-<arch>[-musl]` package that holds the
98
+ * native pacquet binary for the host. On linux the binary packages are
99
+ * split by libc and only the matching one is installed, so spawning and
100
+ * signature verification must agree on this exact name.
101
+ */
102
+ export declare function pacquetPlatformPkgName(): string;
package/lib/runPacquet.js CHANGED
@@ -6,12 +6,34 @@ import readline from 'node:readline';
6
6
  import { PnpmError } from '@pnpm/error';
7
7
  import { logger, streamParser } from '@pnpm/logger';
8
8
  import chalk from 'chalk';
9
+ import { familySync as getLibcFamilySync, MUSL } from 'detect-libc';
9
10
  // The runtime `streamParser` is a `Transform` stream (split2 + JSON.parse).
10
11
  // Its public typing only exposes `on`/`removeListener`, so we narrow to the
11
12
  // writable side here to feed pacquet's NDJSON lines back through the same
12
13
  // parser that `@pnpm/cli.default-reporter` listens on.
13
14
  const streamParserWritable = streamParser;
15
+ /**
16
+ * Build the pacquet install engine `mutateModules` delegates to when
17
+ * `configDependencies` declares pacquet.
18
+ *
19
+ * `run` spawns the pacquet binary installed under
20
+ * `node_modules/.pnpm-config/pacquet`. From `pnpm install`/`pnpm i` it
21
+ * forwards the user's own pnpm CLI flags to pacquet's `install`
22
+ * subcommand; from `add`/`update`/`dedupe` it doesn't forward (warning
23
+ * instead). Pacquet's NDJSON stderr is parsed line-by-line and the
24
+ * valid JSON records are re-emitted on pnpm's global `streamParser` so
25
+ * `@pnpm/cli.default-reporter` renders pacquet's events the same way it
26
+ * renders pnpm's own. Non-JSON stderr lines (panic backtraces,
27
+ * unexpected diagnostics) are forwarded to the real stderr verbatim so
28
+ * they reach the user.
29
+ */
14
30
  export function makeRunPacquet(opts) {
31
+ return {
32
+ supportsResolution: pacquetSupportsResolution(resolvePacquetVersion(opts.lockfileDir, opts.packageName)),
33
+ run: makeRun(opts),
34
+ };
35
+ }
36
+ function makeRun(opts) {
15
37
  return async (callOpts) => {
16
38
  const pacquetBin = resolvePacquetBin(opts.lockfileDir, opts.packageName);
17
39
  // From `pnpm install`/`pnpm i` we forward the user's flags through to
@@ -19,21 +41,23 @@ export function makeRunPacquet(opts) {
19
41
  // surface closely enough on that command that they're safe to pass
20
42
  // along. From `add`/`update`/`dedupe` we don't forward anything: those
21
43
  // commands carry flags pacquet's `install` doesn't recognize
22
- // (`--save-dev`, `--save-peer`, etc.) which clap would reject. Either
23
- // way pacquet picks up the settings users care about from
24
- // `pnpm-workspace.yaml` / `.npmrc` on its own, so a non-install
25
- // delegation isn't broken by the omission.
44
+ // (`--save-dev`, `--save-peer`, etc.) which clap would reject.
26
45
  const forwardedFlags = opts.isInstallCommand ? collectForwardedFlags(opts.argv) : [];
27
- // `--ignore-manifest-check` tells pacquet to skip its per-importer
28
- // `package.json` `pnpm-lock.yaml` freshness gate. pnpm just
29
- // resolved and wrote the lockfile itself; on `pnpm up` / `add` /
30
- // `remove` the manifest on disk is still the pre-mutation copy
31
- // (pnpm writes it after `mutateModules` returns), so pacquet's own
32
- // check would always fire here. See
46
+ // In resolve mode pacquet does the resolution itself, so it must not
47
+ // be pinned to the existing lockfile — drop both injected flags.
48
+ //
49
+ // Otherwise (frozen materialization) inject `--frozen-lockfile` plus
50
+ // `--ignore-manifest-check`. The latter tells pacquet to skip its
51
+ // per-importer `package.json` `pnpm-lock.yaml` freshness gate:
52
+ // pnpm just resolved and wrote the lockfile itself; on `pnpm up` /
53
+ // `add` / `remove` the manifest on disk is still the pre-mutation
54
+ // copy (pnpm writes it after `mutateModules` returns), so pacquet's
55
+ // own check would always fire here. See
33
56
  // https://github.com/pnpm/pnpm/issues/11797. The flag is narrow
34
57
  // (only the manifest check); settings drift like `overrides` is
35
58
  // still enforced and was already re-validated by pnpm.
36
- const args = ['--reporter=ndjson', 'install', '--frozen-lockfile', '--ignore-manifest-check', ...forwardedFlags];
59
+ const frozenArgs = callOpts?.resolve === true ? [] : ['--frozen-lockfile', '--ignore-manifest-check'];
60
+ const args = ['--reporter=ndjson', 'install', ...frozenArgs, ...forwardedFlags];
37
61
  const droppedFlags = opts.isInstallCommand ? [] : collectDroppedFlags(opts.argv);
38
62
  if (droppedFlags.length > 0) {
39
63
  logger.warn({
@@ -53,6 +77,7 @@ export function makeRunPacquet(opts) {
53
77
  logger.info({ message: banner, prefix: opts.lockfileDir });
54
78
  const child = spawn(pacquetBin, args, {
55
79
  cwd: opts.lockfileDir,
80
+ env: makePacquetEnv(opts),
56
81
  stdio: ['ignore', 'inherit', 'pipe'],
57
82
  });
58
83
  const filterResolved = callOpts?.filterResolvedProgress === true;
@@ -89,6 +114,16 @@ export function makeRunPacquet(opts) {
89
114
  });
90
115
  };
91
116
  }
117
+ function makePacquetEnv(opts) {
118
+ const env = { ...process.env };
119
+ for (const key of Object.keys(env)) {
120
+ if (key.toLowerCase() === 'pnpm_config_virtual_store_dir_max_length') {
121
+ delete env[key];
122
+ }
123
+ }
124
+ env.PNPM_CONFIG_VIRTUAL_STORE_DIR_MAX_LENGTH = String(opts.virtualStoreDirMaxLength);
125
+ return env;
126
+ }
92
127
  /**
93
128
  * Path of the platform-specific native pacquet binary for the host. The
94
129
  * pacquet npm package ships a Node wrapper at `bin/pacquet` that uses
@@ -108,7 +143,47 @@ export function makeRunPacquet(opts) {
108
143
  function resolvePacquetBin(lockfileDir, packageName) {
109
144
  const ext = process.platform === 'win32' ? '.exe' : '';
110
145
  const pacquetPkg = fs.realpathSync(path.join(lockfileDir, 'node_modules/.pnpm-config', packageName, 'package.json'));
111
- return createRequire(pacquetPkg).resolve(`@pacquet/${process.platform}-${process.arch}/pacquet${ext}`);
146
+ return createRequire(pacquetPkg).resolve(`${pacquetPlatformPkgName()}/pacquet${ext}`);
147
+ }
148
+ /**
149
+ * Name of the `@pacquet/<platform>-<arch>[-musl]` package that holds the
150
+ * native pacquet binary for the host. On linux the binary packages are
151
+ * split by libc and only the matching one is installed, so spawning and
152
+ * signature verification must agree on this exact name.
153
+ */
154
+ export function pacquetPlatformPkgName() {
155
+ const libc = process.platform === 'linux' && getLibcFamilySync() === MUSL ? '-musl' : '';
156
+ return `@pacquet/${process.platform}-${process.arch}${libc}`;
157
+ }
158
+ /**
159
+ * Read the installed pacquet's version from its `package.json` under
160
+ * `node_modules/.pnpm-config/<packageName>`. Returns `undefined` if it
161
+ * can't be read — callers treat that as "assume the older,
162
+ * materialization-only pacquet" so a missing/garbled manifest degrades
163
+ * to the safe path rather than failing the install.
164
+ */
165
+ function resolvePacquetVersion(lockfileDir, packageName) {
166
+ try {
167
+ const pacquetPkg = fs.realpathSync(path.join(lockfileDir, 'node_modules/.pnpm-config', packageName, 'package.json'));
168
+ const { version } = JSON.parse(fs.readFileSync(pacquetPkg, 'utf8'));
169
+ return version;
170
+ }
171
+ catch {
172
+ return undefined;
173
+ }
174
+ }
175
+ /**
176
+ * pacquet gained full resolving installs in 0.11.7; earlier releases
177
+ * stay on pnpm's resolve-then-materialize path. Pre-release builds of
178
+ * 0.11.7 (e.g. `0.11.7-rc.1`) count as supporting it.
179
+ */
180
+ function pacquetSupportsResolution(version) {
181
+ if (version == null)
182
+ return false;
183
+ const [major, minor, patch] = version.split('.', 3).map((part) => parseInt(part, 10));
184
+ if (Number.isNaN(major) || Number.isNaN(minor) || Number.isNaN(patch))
185
+ return false;
186
+ return major > 0 || (major === 0 && (minor > 11 || (minor === 11 && patch >= 7)));
112
187
  }
113
188
  /**
114
189
  * From `pnpm install`/`pnpm i`, return everything in argv that should
@@ -38,6 +38,8 @@ export function rcOptionsTypes() {
38
38
  'lockfile',
39
39
  'lockfile-include-tarball-url',
40
40
  'network-concurrency',
41
+ 'node-experimental-package-map',
42
+ 'node-package-map-type',
41
43
  'noproxy',
42
44
  'npm-path',
43
45
  'offline',
@@ -135,6 +137,10 @@ dependencies is not found inside the workspace',
135
137
  name: '--interactive',
136
138
  shortAlias: '-i',
137
139
  },
140
+ {
141
+ description: 'Don\'t update the ranges in package.json.',
142
+ name: '--no-save',
143
+ },
138
144
  OPTIONS.globalDir,
139
145
  ...UNIVERSAL_OPTIONS,
140
146
  ],
@@ -205,6 +211,11 @@ async function interactiveUpdate(input, opts, rebuildHandler) {
205
211
  flatChoices.push({
206
212
  name: choice.message,
207
213
  value: choice.value,
214
+ // `name` is the rendered table row (label + versions + workspace + url)
215
+ // that lays out a single choice during selection. After submission
216
+ // @inquirer/prompts comma-joins each choice's `short`, which without
217
+ // this defaults to `name` and dumps the whole table back to stdout.
218
+ short: choice.value,
208
219
  });
209
220
  }
210
221
  }
@@ -271,7 +282,7 @@ async function update(dependencies, opts, rebuildHandler) {
271
282
  else if ((dependencies.length > 0) && dependencies.every(dep => !dep.substring(1).includes('@')) && depth > 0 && !opts.latest) {
272
283
  updateMatching = createMatcher(dependencies);
273
284
  }
274
- return installDeps({
285
+ await installDeps({
275
286
  ...opts,
276
287
  rebuildHandler,
277
288
  allowNew: false,
@@ -284,6 +295,9 @@ async function update(dependencies, opts, rebuildHandler) {
284
295
  updateMatching,
285
296
  updatePackageManifest: opts.save !== false,
286
297
  resolutionMode: opts.save === false ? 'highest' : opts.resolutionMode,
298
+ // `--dry-run` is an `install`-only preview; never let a config-level
299
+ // `dry-run` turn `update` into a no-op check.
300
+ dryRun: false,
287
301
  }, dependencies);
288
302
  }
289
303
  function makeIncludeDependenciesFromCLI(opts) {
@@ -4,6 +4,7 @@ import { PnpmError } from '@pnpm/error';
4
4
  import { readEnvLockfile } from '@pnpm/lockfile.fs';
5
5
  import { logger } from '@pnpm/logger';
6
6
  import { createGetAuthHeaderByURI } from '@pnpm/network.auth-header';
7
+ import { pacquetPlatformPkgName } from './runPacquet.js';
7
8
  /**
8
9
  * Decides whether pnpm may spawn the pacquet binary installed under
9
10
  * `node_modules/.pnpm-config/<packageName>` as an install engine.
@@ -60,7 +61,7 @@ async function collectPacquetPackagesToVerify(packageName, rootDir, registries)
60
61
  return undefined;
61
62
  // Only the host's platform binary is ever spawned, so that's the one whose
62
63
  // identity matters. If it isn't in the lockfile, pacquet couldn't run here.
63
- const platformPkgName = `@pacquet/${process.platform}-${process.arch}`;
64
+ const platformPkgName = pacquetPlatformPkgName();
64
65
  const platformVersion = envLockfile.snapshots[shimKey]?.optionalDependencies?.[platformPkgName];
65
66
  if (platformVersion == null)
66
67
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/installing.commands",
3
- "version": "1100.8.0",
3
+ "version": "1100.10.0",
4
4
  "description": "Commands for installation",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -30,16 +30,17 @@
30
30
  "@inquirer/prompts": "^8.4.3",
31
31
  "@pnpm/colorize-semver-diff": "^2.0.0",
32
32
  "@pnpm/semver-diff": "^2.0.0",
33
- "@pnpm/util.lex-comparator": "^3.0.2",
34
- "@yarnpkg/core": "4.5.0",
33
+ "@pnpm/util.lex-comparator": "^4.0.1",
34
+ "@yarnpkg/core": "4.8.0",
35
35
  "@yarnpkg/lockfile": "^1.1.0",
36
- "@yarnpkg/parsers": "3.0.3",
37
36
  "@zkochan/rimraf": "^4.0.0",
38
37
  "@zkochan/table": "^2.0.1",
39
38
  "chalk": "^5.6.2",
40
39
  "ci-info": "^4.4.0",
40
+ "detect-libc": "^2.1.2",
41
41
  "get-npm-tarball-url": "^2.1.0",
42
42
  "is-subdir": "^2.0.0",
43
+ "js-yaml": "npm:@zkochan/js-yaml@0.0.11",
43
44
  "load-json-file": "^7.0.1",
44
45
  "normalize-path": "^3.0.0",
45
46
  "p-filter": "^4.1.0",
@@ -47,62 +48,65 @@
47
48
  "ramda": "npm:@pnpm/ramda@0.28.1",
48
49
  "render-help": "^2.0.0",
49
50
  "version-selector-type": "^3.0.0",
50
- "@pnpm/building.after-install": "1101.0.21",
51
+ "@pnpm/building.after-install": "1102.0.1",
52
+ "@pnpm/building.policy": "1100.0.10",
53
+ "@pnpm/catalogs.config": "1100.0.1",
51
54
  "@pnpm/cli.command": "1100.0.1",
52
- "@pnpm/catalogs.types": "1100.0.0",
53
55
  "@pnpm/cli.common-cli-options-help": "1100.0.2",
56
+ "@pnpm/cli.utils": "1101.0.12",
54
57
  "@pnpm/config.matcher": "1100.0.1",
55
- "@pnpm/cli.utils": "1101.0.11",
56
- "@pnpm/config.pick-registry-for-package": "1100.0.8",
58
+ "@pnpm/config.pick-registry-for-package": "1100.0.9",
59
+ "@pnpm/config.reader": "1101.10.0",
60
+ "@pnpm/config.writer": "1100.0.13",
57
61
  "@pnpm/constants": "1100.0.0",
58
- "@pnpm/config.reader": "1101.8.0",
59
- "@pnpm/deps.inspection.outdated": "1100.1.7",
60
- "@pnpm/config.writer": "1100.0.12",
61
- "@pnpm/deps.path": "1100.0.7",
62
- "@pnpm/deps.status": "1100.1.0",
62
+ "@pnpm/deps.inspection.outdated": "1100.1.9",
63
+ "@pnpm/deps.path": "1100.0.8",
64
+ "@pnpm/deps.status": "1100.1.2",
65
+ "@pnpm/deps.security.signatures": "1101.2.2",
66
+ "@pnpm/catalogs.types": "1100.0.0",
67
+ "@pnpm/fs.read-modules-dir": "1100.0.1",
63
68
  "@pnpm/error": "1100.0.0",
64
- "@pnpm/deps.security.signatures": "1101.2.1",
69
+ "@pnpm/global.commands": "1100.0.29",
65
70
  "@pnpm/fs.graceful-fs": "1100.1.0",
66
- "@pnpm/fs.read-modules-dir": "1100.0.1",
67
- "@pnpm/hooks.pnpmfile": "1100.0.14",
68
- "@pnpm/installing.context": "1100.0.17",
69
- "@pnpm/installing.dedupe.check": "1100.0.10",
70
- "@pnpm/global.commands": "1100.0.27",
71
- "@pnpm/installing.env-installer": "1101.1.8",
72
- "@pnpm/lockfile.fs": "1100.1.4",
73
- "@pnpm/network.fetch": "1100.1.2",
74
- "@pnpm/pkg-manifest.reader": "1100.0.7",
75
- "@pnpm/pkg-manifest.utils": "1100.2.4",
76
- "@pnpm/resolving.npm-resolver": "1101.5.2",
77
- "@pnpm/installing.deps-installer": "1101.9.0",
71
+ "@pnpm/hooks.pnpmfile": "1100.0.15",
72
+ "@pnpm/installing.dedupe.check": "1100.1.0",
73
+ "@pnpm/installing.dedupe.issues-renderer": "1100.0.1",
74
+ "@pnpm/installing.deps-installer": "1102.1.0",
75
+ "@pnpm/installing.context": "1100.0.19",
76
+ "@pnpm/lockfile.fs": "1100.1.6",
77
+ "@pnpm/lockfile.types": "1100.0.11",
78
+ "@pnpm/installing.env-installer": "1102.0.1",
79
+ "@pnpm/network.fetch": "1100.1.3",
80
+ "@pnpm/pkg-manifest.reader": "1100.0.8",
81
+ "@pnpm/network.auth-header": "1101.1.2",
78
82
  "@pnpm/resolving.parse-wanted-dependency": "1100.0.1",
79
- "@pnpm/resolving.resolver-base": "1100.4.1",
80
- "@pnpm/store.connection-manager": "1100.2.8",
81
- "@pnpm/network.auth-header": "1101.1.1",
82
- "@pnpm/store.controller": "1101.0.13",
83
- "@pnpm/types": "1101.3.1",
84
- "@pnpm/workspace.project-manifest-reader": "1100.0.12",
85
- "@pnpm/workspace.project-manifest-writer": "1100.0.7",
86
- "@pnpm/workspace.projects-graph": "1100.0.17",
87
- "@pnpm/workspace.projects-reader": "1101.0.11",
88
- "@pnpm/workspace.projects-sorter": "1100.0.6",
89
- "@pnpm/workspace.root-finder": "1100.0.1",
90
- "@pnpm/workspace.state": "1100.0.21",
91
- "@pnpm/workspace.workspace-manifest-writer": "1100.0.12",
92
- "@pnpm/workspace.projects-filter": "1100.0.20",
93
- "@pnpm/building.policy": "1100.0.9",
94
- "@pnpm/lockfile.types": "1100.0.10"
83
+ "@pnpm/resolving.npm-resolver": "1102.0.1",
84
+ "@pnpm/store.connection-manager": "1100.3.1",
85
+ "@pnpm/resolving.resolver-base": "1100.4.2",
86
+ "@pnpm/workspace.project-manifest-reader": "1100.0.13",
87
+ "@pnpm/pkg-manifest.utils": "1100.2.5",
88
+ "@pnpm/types": "1101.3.2",
89
+ "@pnpm/workspace.project-manifest-writer": "1100.0.8",
90
+ "@pnpm/workspace.projects-sorter": "1100.0.7",
91
+ "@pnpm/workspace.projects-graph": "1100.0.19",
92
+ "@pnpm/workspace.projects-reader": "1101.0.12",
93
+ "@pnpm/workspace.state": "1100.0.23",
94
+ "@pnpm/workspace.projects-filter": "1100.0.22",
95
+ "@pnpm/workspace.root-finder": "1100.0.2",
96
+ "@pnpm/store.controller": "1102.0.1",
97
+ "@pnpm/workspace.workspace-manifest-writer": "1100.0.13"
95
98
  },
96
99
  "peerDependencies": {
97
- "@pnpm/logger": "^1001.0.1"
100
+ "@pnpm/logger": "^1100.0.0"
98
101
  },
99
102
  "devDependencies": {
100
- "@jest/globals": "30.3.0",
103
+ "@jest/globals": "30.4.1",
104
+ "@types/js-yaml": "^4.0.9",
101
105
  "@types/normalize-path": "^3.0.2",
102
106
  "@types/proxyquire": "^1.3.31",
103
107
  "@types/ramda": "0.31.1",
104
108
  "@types/yarnpkg__lockfile": "^1.1.9",
105
- "@types/zkochan__table": "npm:@types/table@6.0.0",
109
+ "@types/zkochan__table": "npm:@types/table@6.3.2",
106
110
  "delay": "^7.0.0",
107
111
  "jest-diff": "^30.4.1",
108
112
  "path-name": "^1.0.0",
@@ -113,19 +117,19 @@
113
117
  "write-json-file": "^7.0.0",
114
118
  "write-package": "7.2.0",
115
119
  "write-yaml-file": "^6.0.0",
116
- "@pnpm/assert-project": "1100.0.15",
117
- "@pnpm/installing.commands": "1100.8.0",
120
+ "@pnpm/installing.commands": "1100.10.0",
121
+ "@pnpm/installing.modules-yaml": "1100.0.9",
118
122
  "@pnpm/logger": "1100.0.0",
119
- "@pnpm/installing.modules-yaml": "1100.0.8",
120
- "@pnpm/store.index": "1100.1.0",
121
- "@pnpm/prepare": "1100.0.15",
122
- "@pnpm/test-fixtures": "1100.0.0",
123
+ "@pnpm/assert-project": "1100.0.16",
124
+ "@pnpm/store.index": "1100.2.0",
123
125
  "@pnpm/test-ipc-server": "1100.0.0",
124
- "@pnpm/testing.mock-agent": "1101.0.2",
125
- "@pnpm/testing.command-defaults": "1100.0.5",
126
- "@pnpm/worker": "1100.1.11",
127
- "@pnpm/workspace.projects-filter": "1100.0.20",
128
- "@pnpm/testing.registry-mock": "1100.0.5"
126
+ "@pnpm/prepare": "1100.0.16",
127
+ "@pnpm/testing.registry-mock": "1100.0.6",
128
+ "@pnpm/testing.command-defaults": "1100.0.6",
129
+ "@pnpm/testing.mock-agent": "1101.0.3",
130
+ "@pnpm/test-fixtures": "1100.0.0",
131
+ "@pnpm/worker": "1100.2.1",
132
+ "@pnpm/workspace.projects-filter": "1100.0.22"
129
133
  },
130
134
  "engines": {
131
135
  "node": ">=22.13"