@pnpm/installing.commands 1100.2.2 → 1100.4.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 +5 -1
- package/lib/fetch.js +1 -0
- package/lib/import/index.js +1 -0
- package/lib/install.js +5 -0
- package/lib/installDeps.d.ts +1 -1
- package/lib/installDeps.js +68 -11
- package/lib/link.js +3 -3
- package/lib/policyHandlers.d.ts +68 -0
- package/lib/policyHandlers.js +162 -0
- package/lib/recursive.d.ts +10 -1
- package/lib/recursive.js +41 -7
- package/lib/remove.js +1 -0
- package/lib/resolutionPolicyManifest.d.ts +6 -0
- package/lib/resolutionPolicyManifest.js +17 -0
- package/lib/runPacquet.d.ts +48 -0
- package/lib/runPacquet.js +126 -0
- package/lib/update/index.js +5 -1
- package/package.json +43 -42
package/lib/add.js
CHANGED
|
@@ -10,6 +10,7 @@ import { pick } from 'ramda';
|
|
|
10
10
|
import { renderHelp } from 'render-help';
|
|
11
11
|
import { getFetchFullMetadata } from './getFetchFullMetadata.js';
|
|
12
12
|
import { installDeps } from './installDeps.js';
|
|
13
|
+
import { createGlobalPolicyCallbacks } from './resolutionPolicyManifest.js';
|
|
13
14
|
export const shorthands = {
|
|
14
15
|
'save-catalog': '--save-catalog-name=default',
|
|
15
16
|
d: '--save-dev',
|
|
@@ -228,7 +229,10 @@ export async function handler(opts, params, commands) {
|
|
|
228
229
|
if (params.includes('pnpm') || params.includes('@pnpm/exe')) {
|
|
229
230
|
throw new PnpmError('GLOBAL_PNPM_INSTALL', 'Use the "pnpm self-update" command to install or update pnpm');
|
|
230
231
|
}
|
|
231
|
-
return handleGlobalAdd(
|
|
232
|
+
return handleGlobalAdd({
|
|
233
|
+
...opts,
|
|
234
|
+
...createGlobalPolicyCallbacks(opts),
|
|
235
|
+
}, params, commands ?? {});
|
|
232
236
|
}
|
|
233
237
|
const include = {
|
|
234
238
|
dependencies: opts.production !== false,
|
package/lib/fetch.js
CHANGED
|
@@ -58,6 +58,7 @@ export async function handler(opts) {
|
|
|
58
58
|
pruneStore: true,
|
|
59
59
|
storeController: store.ctrl,
|
|
60
60
|
storeDir: store.dir,
|
|
61
|
+
resolutionVerifiers: store.resolutionVerifiers,
|
|
61
62
|
// Hoisting is skipped anyway,
|
|
62
63
|
// so we store these empty patterns in node_modules/.modules.yaml
|
|
63
64
|
// to let the subsequent install know that hoisting should be performed.
|
package/lib/import/index.js
CHANGED
package/lib/install.js
CHANGED
|
@@ -79,6 +79,11 @@ export const cliOptionsTypes = () => ({
|
|
|
79
79
|
'fix-lockfile': Boolean,
|
|
80
80
|
'resolution-only': Boolean,
|
|
81
81
|
recursive: Boolean,
|
|
82
|
+
// `--no-save` lets `pnpm install` skip writing to package.json /
|
|
83
|
+
// pnpm-workspace.yaml. Without registering it here, nopt drops the
|
|
84
|
+
// flag, `opts.save` stays undefined, and the auto-add path treats
|
|
85
|
+
// it as "save enabled".
|
|
86
|
+
save: Boolean,
|
|
82
87
|
});
|
|
83
88
|
export const shorthands = {
|
|
84
89
|
D: '--dev',
|
package/lib/installDeps.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { 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' | 'agent' | 'production' | 'preferWorkspacePackages' | 'registries' | 'runtime' | 'runtimeOnFail' | 'save' | 'saveDev' | 'saveExact' | 'saveOptional' | 'savePeer' | 'savePrefix' | 'saveProd' | 'saveWorkspaceProtocol' | 'lockfileIncludeTarballUrl' | 'scriptsPrependNodePath' | 'scriptShell' | 'sideEffectsCache' | 'sideEffectsCacheReadonly' | 'sort' | 'sharedWorkspaceLockfile' | 'shellEmulator' | 'tag' | 'allowBuilds' | 'optional' | 'workspaceConcurrency' | 'workspaceDir' | 'workspacePackagePatterns' | 'extraEnv' | 'ignoreWorkspaceCycles' | 'disallowWorkspaceCycles' | 'configDependencies' | 'packageExtensions' | 'updateConfig'> & Pick<ConfigContext, 'allProjects' | 'allProjectsGraph' | 'cliOptions' | 'hooks' | 'rootProjectManifestDir' | 'rootProjectManifest' | 'selectedProjectsGraph'> & 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' | 'agent' | 'production' | 'preferWorkspacePackages' | 'registries' | 'runtime' | 'runtimeOnFail' | 'save' | 'saveDev' | 'saveExact' | 'saveOptional' | 'savePeer' | 'savePrefix' | 'saveProd' | 'saveWorkspaceProtocol' | 'lockfileIncludeTarballUrl' | 'scriptsPrependNodePath' | 'scriptShell' | 'sideEffectsCache' | 'sideEffectsCacheReadonly' | 'sort' | 'sharedWorkspaceLockfile' | 'shellEmulator' | 'tag' | '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 & {
|
|
8
8
|
argv: {
|
|
9
9
|
original: string[];
|
|
10
10
|
};
|
package/lib/installDeps.js
CHANGED
|
@@ -17,7 +17,9 @@ import { updateWorkspaceManifest } from '@pnpm/workspace.workspace-manifest-writ
|
|
|
17
17
|
import { getPinnedVersion } from './getPinnedVersion.js';
|
|
18
18
|
import { getSaveType } from './getSaveType.js';
|
|
19
19
|
import { handleIgnoredBuilds } from './handleIgnoredBuilds.js';
|
|
20
|
+
import { setupPolicyHandlers } from './policyHandlers.js';
|
|
20
21
|
import { createMatcher, makeIgnorePatterns, matchDependencies, recursive, } from './recursive.js';
|
|
22
|
+
import { makeRunPacquet } from './runPacquet.js';
|
|
21
23
|
import { createWorkspaceSpecs, updateToWorkspacePackagesFromManifest } from './updateWorkspaceDependencies.js';
|
|
22
24
|
const OVERWRITE_UPDATE_OPTIONS = {
|
|
23
25
|
allowNew: true,
|
|
@@ -54,6 +56,29 @@ export async function installDeps(opts, params) {
|
|
|
54
56
|
opts['preserveWorkspaceProtocol'] = !opts.linkWorkspacePackages;
|
|
55
57
|
}
|
|
56
58
|
const store = await createStoreController(opts);
|
|
59
|
+
// When `configDependencies` declares pacquet, build the alternative
|
|
60
|
+
// install engine the deps-installer delegates to. The CLI layer owns
|
|
61
|
+
// the construction so the installer doesn't need to know about
|
|
62
|
+
// pacquet's binary path, CLI surface, or any settings that only
|
|
63
|
+
// pacquet consumes. Threaded through both the workspace recursive
|
|
64
|
+
// path and the single-project path below. Two declaration names are
|
|
65
|
+
// accepted: the original unscoped `pacquet` and the official scoped
|
|
66
|
+
// `@pnpm/pacquet` mirror. Both packages ship the same JS shim and
|
|
67
|
+
// optional `@pacquet/<plat>-<arch>` binary sub-packages, so the
|
|
68
|
+
// resolved \`node_modules/.pnpm-config/<name>\` layout pacquet's
|
|
69
|
+
// wrapper expects is identical either way.
|
|
70
|
+
const pacquetConfigDepName = opts.configDependencies?.['@pnpm/pacquet'] != null
|
|
71
|
+
? '@pnpm/pacquet'
|
|
72
|
+
: opts.configDependencies?.pacquet != null
|
|
73
|
+
? 'pacquet'
|
|
74
|
+
: undefined;
|
|
75
|
+
const runPacquet = pacquetConfigDepName != null
|
|
76
|
+
? makeRunPacquet({
|
|
77
|
+
lockfileDir: opts.lockfileDir ?? opts.dir,
|
|
78
|
+
packageName: pacquetConfigDepName,
|
|
79
|
+
argv: opts.argv.original,
|
|
80
|
+
})
|
|
81
|
+
: undefined;
|
|
57
82
|
const includeDirect = opts.includeDirect ?? {
|
|
58
83
|
dependencies: true,
|
|
59
84
|
devDependencies: true,
|
|
@@ -94,6 +119,7 @@ export async function installDeps(opts, params) {
|
|
|
94
119
|
selectedProjectsGraph,
|
|
95
120
|
storeControllerAndDir: store,
|
|
96
121
|
workspaceDir: opts.workspaceDir,
|
|
122
|
+
runPacquet,
|
|
97
123
|
}, opts.update ? 'update' : (params.length === 0 ? 'install' : 'add'));
|
|
98
124
|
return;
|
|
99
125
|
}
|
|
@@ -115,6 +141,12 @@ export async function installDeps(opts, params) {
|
|
|
115
141
|
else if (opts.runtimeOnFail) {
|
|
116
142
|
applyRuntimeOnFailOverride(manifest, opts.runtimeOnFail);
|
|
117
143
|
}
|
|
144
|
+
// `setupPolicyHandlers` composes the per-policy handlers the install
|
|
145
|
+
// needs for the current opts (today: minimumReleaseAge; future:
|
|
146
|
+
// trustPolicy UX, license policy, etc.). Returns `undefined` when no
|
|
147
|
+
// handler is active so the install skips the empty no-op call at
|
|
148
|
+
// every checkpoint when no policies are configured.
|
|
149
|
+
const policyHandlers = setupPolicyHandlers(opts);
|
|
118
150
|
const installOpts = {
|
|
119
151
|
...opts,
|
|
120
152
|
// In case installation is done in a multi-package repository
|
|
@@ -127,8 +159,11 @@ export async function installDeps(opts, params) {
|
|
|
127
159
|
skipRuntimes: opts.runtime === false,
|
|
128
160
|
storeController: store.ctrl,
|
|
129
161
|
storeDir: store.dir,
|
|
162
|
+
resolutionVerifiers: store.resolutionVerifiers,
|
|
130
163
|
workspacePackages,
|
|
131
164
|
preferredVersions: opts.packageVulnerabilityAudit ? preferNonvulnerablePackageVersions(opts.packageVulnerabilityAudit) : undefined,
|
|
165
|
+
handleResolutionPolicyViolations: policyHandlers?.handleResolutionPolicyViolations,
|
|
166
|
+
runPacquet,
|
|
132
167
|
};
|
|
133
168
|
let updateMatch;
|
|
134
169
|
let updatePackageManifest = opts.updatePackageManifest;
|
|
@@ -187,14 +222,20 @@ export async function installDeps(opts, params) {
|
|
|
187
222
|
rootDir: opts.dir,
|
|
188
223
|
targetDependenciesField: getSaveType(opts),
|
|
189
224
|
};
|
|
190
|
-
const { updatedCatalogs, updatedProject, ignoredBuilds } = await mutateModulesInSingleProject(mutatedProject, installOpts);
|
|
225
|
+
const { updatedCatalogs, updatedProject, ignoredBuilds, resolutionPolicyViolations } = await mutateModulesInSingleProject(mutatedProject, installOpts);
|
|
191
226
|
if (opts.save !== false) {
|
|
227
|
+
// Only pick entries when we'll actually persist. Otherwise the
|
|
228
|
+
// info log would claim we added entries the workspace manifest
|
|
229
|
+
// never saw, and the next install would re-prompt or fail
|
|
230
|
+
// verification.
|
|
231
|
+
const policyUpdates = policyHandlers?.pickManifestUpdates(resolutionPolicyViolations);
|
|
192
232
|
await Promise.all([
|
|
193
233
|
writeProjectManifest(updatedProject.manifest),
|
|
194
234
|
updateWorkspaceManifest(opts.workspaceDir ?? opts.dir, {
|
|
195
235
|
updatedCatalogs,
|
|
196
236
|
cleanupUnusedCatalogs: opts.cleanupUnusedCatalogs,
|
|
197
237
|
allProjects: opts.allProjects,
|
|
238
|
+
...policyUpdates,
|
|
198
239
|
}),
|
|
199
240
|
]);
|
|
200
241
|
}
|
|
@@ -211,20 +252,35 @@ export async function installDeps(opts, params) {
|
|
|
211
252
|
await handleIgnoredBuilds(opts, ignoredBuilds);
|
|
212
253
|
return;
|
|
213
254
|
}
|
|
214
|
-
const { updatedCatalogs, updatedManifest, ignoredBuilds } = await install(manifest, {
|
|
255
|
+
const { updatedCatalogs, updatedManifest, ignoredBuilds, resolutionPolicyViolations } = await install(manifest, {
|
|
215
256
|
...installOpts,
|
|
216
257
|
updatePackageManifest,
|
|
217
258
|
updateMatching,
|
|
218
259
|
});
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
260
|
+
// `opts.save === false` (e.g. `--no-save`) means "don't persist anything
|
|
261
|
+
// from this install" — both package.json and the workspace manifest.
|
|
262
|
+
// Skip the pick so the info log doesn't claim entries were added that
|
|
263
|
+
// were never written; the next install will resurface them.
|
|
264
|
+
if (opts.save !== false) {
|
|
265
|
+
const policyUpdates = policyHandlers?.pickManifestUpdates(resolutionPolicyViolations);
|
|
266
|
+
if (opts.update === true) {
|
|
267
|
+
await Promise.all([
|
|
268
|
+
writeProjectManifest(updatedManifest),
|
|
269
|
+
updateWorkspaceManifest(opts.workspaceDir ?? opts.dir, {
|
|
270
|
+
updatedCatalogs,
|
|
271
|
+
cleanupUnusedCatalogs: opts.cleanupUnusedCatalogs,
|
|
272
|
+
allProjects,
|
|
273
|
+
...policyUpdates,
|
|
274
|
+
}),
|
|
275
|
+
]);
|
|
276
|
+
}
|
|
277
|
+
else if (policyUpdates != null) {
|
|
278
|
+
// Plain `pnpm install` (no --update, no params) wouldn't otherwise touch
|
|
279
|
+
// the workspace manifest. Persist the auto-policy patches anyway so any
|
|
280
|
+
// loose bypass (today: minimumReleaseAgeExclude) remains explicit on
|
|
281
|
+
// subsequent installs.
|
|
282
|
+
await updateWorkspaceManifest(opts.workspaceDir ?? opts.dir, policyUpdates);
|
|
283
|
+
}
|
|
228
284
|
}
|
|
229
285
|
await handleIgnoredBuilds(opts, ignoredBuilds);
|
|
230
286
|
if (opts.linkWorkspacePackages && opts.workspaceDir) {
|
|
@@ -243,6 +299,7 @@ export async function installDeps(opts, params) {
|
|
|
243
299
|
allProjectsGraph: opts.allProjectsGraph,
|
|
244
300
|
selectedProjectsGraph,
|
|
245
301
|
workspaceDir: opts.workspaceDir, // Otherwise TypeScript doesn't understand that is not undefined
|
|
302
|
+
runPacquet,
|
|
246
303
|
}, 'install');
|
|
247
304
|
if (opts.ignoreScripts)
|
|
248
305
|
return;
|
package/lib/link.js
CHANGED
|
@@ -69,6 +69,9 @@ This might cause issues in your project. To resolve this, you may use the "file:
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
export async function handler(opts, params) {
|
|
72
|
+
if ((params == null) || (params.length === 0)) {
|
|
73
|
+
throw new PnpmError('LINK_BAD_PARAMS', 'You must provide a parameter. Usage: pnpm link <dir>');
|
|
74
|
+
}
|
|
72
75
|
let workspacePackagesArr;
|
|
73
76
|
let workspacePackages;
|
|
74
77
|
if (opts.workspaceDir) {
|
|
@@ -87,9 +90,6 @@ export async function handler(opts, params) {
|
|
|
87
90
|
binsDir: opts.bin,
|
|
88
91
|
});
|
|
89
92
|
const writeProjectManifest = await createProjectManifestWriter(opts.rootProjectManifestDir);
|
|
90
|
-
if ((params == null) || (params.length === 0)) {
|
|
91
|
-
throw new PnpmError('LINK_BAD_PARAMS', 'You must provide a parameter. Usage: pnpm link <dir>');
|
|
92
|
-
}
|
|
93
93
|
const [pkgPaths, pkgNames] = partition((inp) => isFilespec.test(inp), params);
|
|
94
94
|
if (pkgNames.length > 0) {
|
|
95
95
|
throw new PnpmError('LINK_BAD_PARAMS', `Cannot link by package name. Use a relative or absolute path instead, e.g. "pnpm link ./${pkgNames[0]}"`);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shape returned by `installing/deps-installer`'s
|
|
3
|
+
* `collectResolutionPolicyViolations` and the inline accumulator on
|
|
4
|
+
* the resolveDependencies result. Re-declared locally so the commands
|
|
5
|
+
* layer can react without depending on the deps-installer's private
|
|
6
|
+
* install types.
|
|
7
|
+
*
|
|
8
|
+
* Verifier codes (today: `MINIMUM_RELEASE_AGE_VIOLATION` and
|
|
9
|
+
* `TRUST_DOWNGRADE`) are the contract surface for downstream UX.
|
|
10
|
+
* Each `PolicyHandler` below filters violations by code to decide
|
|
11
|
+
* what to do with them (prompt, persist to an exclude list, log,
|
|
12
|
+
* abort).
|
|
13
|
+
*/
|
|
14
|
+
export interface PolicyViolation {
|
|
15
|
+
name: string;
|
|
16
|
+
version: string;
|
|
17
|
+
code: string;
|
|
18
|
+
reason: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Workspace-manifest patch a per-policy handler can request. Each
|
|
22
|
+
* field maps to a `pnpm-workspace.yaml` exclude-list array; the
|
|
23
|
+
* install command forwards these to `updateWorkspaceManifest` so the
|
|
24
|
+
* workspace writer dedupes and appends them in one pass.
|
|
25
|
+
*
|
|
26
|
+
* New policies that want auto-persistence add their field here AND
|
|
27
|
+
* teach `updateWorkspaceManifest` how to honor it.
|
|
28
|
+
*/
|
|
29
|
+
export interface WorkspaceManifestPolicyUpdates {
|
|
30
|
+
addedMinimumReleaseAgeExcludes?: string[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Aggregated plan the install command consumes. The `handleResolutionPolicyViolations`
|
|
34
|
+
* call fans out across every registered handler in registration order;
|
|
35
|
+
* any handler can throw to abort. `pickManifestUpdates` merges the
|
|
36
|
+
* per-handler patches into one bag so the workspace writer runs once.
|
|
37
|
+
*/
|
|
38
|
+
export interface PolicyHandlersPlan {
|
|
39
|
+
handleResolutionPolicyViolations: (violations: readonly PolicyViolation[]) => Promise<void>;
|
|
40
|
+
pickManifestUpdates: (violations: readonly PolicyViolation[]) => WorkspaceManifestPolicyUpdates | undefined;
|
|
41
|
+
}
|
|
42
|
+
export interface PolicyHandlersOptions {
|
|
43
|
+
minimumReleaseAge?: number;
|
|
44
|
+
minimumReleaseAgeStrict?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Pass `false` for `--no-save` installs. Handlers that would
|
|
47
|
+
* persist to the workspace manifest refuse to enter modes where
|
|
48
|
+
* approval is durably required (today: strict minimumReleaseAge)
|
|
49
|
+
* so the prompt never offers an action it can't honor.
|
|
50
|
+
*/
|
|
51
|
+
save?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Override for CI detection. Defaults to `ci-info`'s `isCI` flag.
|
|
54
|
+
*/
|
|
55
|
+
ci?: boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Composes the per-policy handlers the install command needs for the
|
|
59
|
+
* current opts. Returns `undefined` only when no handler reports
|
|
60
|
+
* activity — saves the install command an empty no-op call at every
|
|
61
|
+
* checkpoint when no policies are configured.
|
|
62
|
+
*
|
|
63
|
+
* Today only the minimumReleaseAge handler is registered. Future
|
|
64
|
+
* policies (trustPolicy UX, license policy, etc.) plug in by
|
|
65
|
+
* exporting a sibling `create<Name>PolicyHandler(opts)` and getting
|
|
66
|
+
* pushed into the `handlers` list below.
|
|
67
|
+
*/
|
|
68
|
+
export declare function setupPolicyHandlers(opts: PolicyHandlersOptions): PolicyHandlersPlan | undefined;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { PnpmError } from '@pnpm/error';
|
|
2
|
+
import { globalInfo } from '@pnpm/logger';
|
|
3
|
+
import { MINIMUM_RELEASE_AGE_VIOLATION_CODE } from '@pnpm/resolving.npm-resolver';
|
|
4
|
+
import { isCI } from 'ci-info';
|
|
5
|
+
import enquirer from 'enquirer';
|
|
6
|
+
/**
|
|
7
|
+
* Composes the per-policy handlers the install command needs for the
|
|
8
|
+
* current opts. Returns `undefined` only when no handler reports
|
|
9
|
+
* activity — saves the install command an empty no-op call at every
|
|
10
|
+
* checkpoint when no policies are configured.
|
|
11
|
+
*
|
|
12
|
+
* Today only the minimumReleaseAge handler is registered. Future
|
|
13
|
+
* policies (trustPolicy UX, license policy, etc.) plug in by
|
|
14
|
+
* exporting a sibling `create<Name>PolicyHandler(opts)` and getting
|
|
15
|
+
* pushed into the `handlers` list below.
|
|
16
|
+
*/
|
|
17
|
+
export function setupPolicyHandlers(opts) {
|
|
18
|
+
const handlers = [];
|
|
19
|
+
const minimumReleaseAge = createMinimumReleaseAgeHandler(opts);
|
|
20
|
+
if (minimumReleaseAge)
|
|
21
|
+
handlers.push(minimumReleaseAge);
|
|
22
|
+
if (handlers.length === 0)
|
|
23
|
+
return undefined;
|
|
24
|
+
return {
|
|
25
|
+
handleResolutionPolicyViolations: async (violations) => {
|
|
26
|
+
// Sequential, not parallel: a TTY prompt from handler N would
|
|
27
|
+
// race with a different prompt from N+1, and we want a clean
|
|
28
|
+
// throw to short-circuit before later handlers ask for input.
|
|
29
|
+
for (const handler of handlers) {
|
|
30
|
+
if (handler.handleResolutionPolicyViolations) {
|
|
31
|
+
// eslint-disable-next-line no-await-in-loop
|
|
32
|
+
await handler.handleResolutionPolicyViolations(violations);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
pickManifestUpdates: (violations) => {
|
|
37
|
+
const merged = {};
|
|
38
|
+
let any = false;
|
|
39
|
+
for (const handler of handlers) {
|
|
40
|
+
if (!handler.pickManifestUpdates)
|
|
41
|
+
continue;
|
|
42
|
+
const patch = handler.pickManifestUpdates(violations);
|
|
43
|
+
if (patch == null)
|
|
44
|
+
continue;
|
|
45
|
+
// Shallow merge — handlers own disjoint fields by convention,
|
|
46
|
+
// so there's no collision policy to encode here yet.
|
|
47
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
48
|
+
if (value == null)
|
|
49
|
+
continue;
|
|
50
|
+
merged[key] = value;
|
|
51
|
+
any = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return any ? merged : undefined;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* minimumReleaseAge policy handler.
|
|
60
|
+
*
|
|
61
|
+
* Loose mode (`minimumReleaseAgeStrict: false`) lets the resolver
|
|
62
|
+
* install versions newer than the cutoff and auto-persists them to
|
|
63
|
+
* `minimumReleaseAgeExclude`. Strict mode + an interactive TTY
|
|
64
|
+
* surfaces the full set of immature picks (direct AND transitive) at
|
|
65
|
+
* once via a confirm prompt — the install proceeds if the user
|
|
66
|
+
* approves, otherwise it aborts before touching the lockfile or
|
|
67
|
+
* package.json (#10488). Strict mode in CI or any other non-TTY
|
|
68
|
+
* context aborts hard with the same violation list so the failure
|
|
69
|
+
* pinpoints every offending entry, not just the first one the
|
|
70
|
+
* resolver picked.
|
|
71
|
+
*
|
|
72
|
+
* Strict mode combined with `--no-save` is rejected up-front — the
|
|
73
|
+
* approval prompt promises persistence the install command's
|
|
74
|
+
* `opts.save !== false` gate would block, leaving the lockfile
|
|
75
|
+
* holding approved-but-unlisted immature picks that the next install
|
|
76
|
+
* would reject.
|
|
77
|
+
*
|
|
78
|
+
* Returns `undefined` when minimumReleaseAge is not active.
|
|
79
|
+
*/
|
|
80
|
+
function createMinimumReleaseAgeHandler(opts) {
|
|
81
|
+
if (!opts.minimumReleaseAge)
|
|
82
|
+
return undefined;
|
|
83
|
+
const strictMode = opts.minimumReleaseAgeStrict === true;
|
|
84
|
+
const persistenceEnabled = opts.save !== false;
|
|
85
|
+
const inCi = opts.ci ?? isCI;
|
|
86
|
+
const canPrompt = !inCi && Boolean(process.stdin.isTTY);
|
|
87
|
+
return {
|
|
88
|
+
handleResolutionPolicyViolations: async (violations) => {
|
|
89
|
+
if (!strictMode)
|
|
90
|
+
return;
|
|
91
|
+
const immature = filterImmatureViolations(violations);
|
|
92
|
+
if (immature.length === 0)
|
|
93
|
+
return;
|
|
94
|
+
if (!persistenceEnabled) {
|
|
95
|
+
throw new PnpmError('STRICT_MIN_RELEASE_AGE_REQUIRES_SAVE', 'minimumReleaseAgeStrict cannot be combined with --no-save: ' +
|
|
96
|
+
'approval would require writing to minimumReleaseAgeExclude in pnpm-workspace.yaml, ' +
|
|
97
|
+
'which --no-save prevents.', {
|
|
98
|
+
hint: 'Drop --no-save so the exclude list can be persisted, or set ' +
|
|
99
|
+
'minimumReleaseAgeStrict: false to let the install proceed without prompting ' +
|
|
100
|
+
'(the lockfile would still trigger the auto-collect on the next normal install).',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (canPrompt) {
|
|
104
|
+
await promptForApproval(immature);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
throw failOnImmature(immature);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
pickManifestUpdates: (violations) => {
|
|
111
|
+
const entries = pickImmatureEntries(violations, strictMode);
|
|
112
|
+
return entries ? { addedMinimumReleaseAgeExcludes: entries } : undefined;
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function filterImmatureViolations(violations) {
|
|
117
|
+
return violations.filter((v) => v.code === MINIMUM_RELEASE_AGE_VIOLATION_CODE);
|
|
118
|
+
}
|
|
119
|
+
function pickImmatureEntries(violations, promptRequired) {
|
|
120
|
+
const immature = filterImmatureViolations(violations);
|
|
121
|
+
if (immature.length === 0)
|
|
122
|
+
return undefined;
|
|
123
|
+
const sorted = [...new Set(immature.map((v) => `${v.name}@${v.version}`))].sort();
|
|
124
|
+
// Strict-mode picks already passed through the approval prompt, so
|
|
125
|
+
// the log here only confirms what was persisted. Loose-mode picks
|
|
126
|
+
// haven't been announced anywhere else, so the same log doubles as
|
|
127
|
+
// the discovery notice.
|
|
128
|
+
const reason = promptRequired
|
|
129
|
+
? '(approved at the prompt)'
|
|
130
|
+
: '(loose mode allowed these immature versions)';
|
|
131
|
+
globalInfo(`Added ${sorted.length} ${sorted.length === 1 ? 'entry' : 'entries'} to minimumReleaseAgeExclude in pnpm-workspace.yaml ` +
|
|
132
|
+
`${reason}:\n ${sorted.join('\n ')}`);
|
|
133
|
+
return sorted;
|
|
134
|
+
}
|
|
135
|
+
function failOnImmature(immature) {
|
|
136
|
+
const sorted = [...immature].sort((a, b) => `${a.name}@${a.version}`.localeCompare(`${b.name}@${b.version}`));
|
|
137
|
+
const list = sorted.map((v) => ` ${v.name}@${v.version} ${v.reason}`).join('\n');
|
|
138
|
+
return new PnpmError('NO_MATURE_MATCHING_VERSION', `${sorted.length} ${sorted.length === 1 ? 'version does' : 'versions do'} not meet the minimumReleaseAge constraint:\n${list}`, {
|
|
139
|
+
hint: 'Run the install interactively to approve these picks, or add them to ' +
|
|
140
|
+
'minimumReleaseAgeExclude in pnpm-workspace.yaml, or wait for the packages ' +
|
|
141
|
+
'to mature past the configured cutoff.',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async function promptForApproval(immature) {
|
|
145
|
+
const sorted = [...immature].sort((a, b) => `${a.name}@${a.version}`.localeCompare(`${b.name}@${b.version}`));
|
|
146
|
+
const message = `${sorted.length} ${sorted.length === 1 ? 'version does' : 'versions do'} not meet the minimumReleaseAge constraint:\n` +
|
|
147
|
+
sorted.map((v) => ` ${v.name}@${v.version}`).join('\n') + '\n' +
|
|
148
|
+
'Add to minimumReleaseAgeExclude in pnpm-workspace.yaml and proceed with the install?';
|
|
149
|
+
const answer = await enquirer.prompt({
|
|
150
|
+
type: 'confirm',
|
|
151
|
+
name: 'confirmed',
|
|
152
|
+
message,
|
|
153
|
+
initial: false,
|
|
154
|
+
});
|
|
155
|
+
if (!answer.confirmed) {
|
|
156
|
+
throw new PnpmError('MINIMUM_RELEASE_AGE_DENIED', 'Aborted: the immature versions were not approved.', {
|
|
157
|
+
hint: 'Re-run the install without `minimumReleaseAgeStrict: true` to allow these versions, ' +
|
|
158
|
+
'or wait for the packages to mature past the configured cutoff.',
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=policyHandlers.js.map
|
package/lib/recursive.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { CommandHandler } from '@pnpm/cli.command';
|
|
|
2
2
|
import { type Config, type ConfigContext } from '@pnpm/config.reader';
|
|
3
3
|
import { type UpdateMatchingFunction } from '@pnpm/installing.deps-installer';
|
|
4
4
|
import type { PreferredVersions } from '@pnpm/resolving.resolver-base';
|
|
5
|
+
import type { ResolutionVerifier } from '@pnpm/resolving.resolver-base';
|
|
5
6
|
import { type CreateStoreControllerOptions } from '@pnpm/store.connection-manager';
|
|
6
7
|
import type { StoreController } from '@pnpm/store.controller';
|
|
7
8
|
import type { IncludedDependencies, Project, ProjectManifest, ProjectsGraph } from '@pnpm/types';
|
|
@@ -26,9 +27,17 @@ export type RecursiveOptions = CreateStoreControllerOptions & Pick<Config, 'bail
|
|
|
26
27
|
storeControllerAndDir?: {
|
|
27
28
|
ctrl: StoreController;
|
|
28
29
|
dir: string;
|
|
30
|
+
resolutionVerifiers: ResolutionVerifier[];
|
|
29
31
|
};
|
|
30
32
|
pnpmfile: string[];
|
|
31
|
-
|
|
33
|
+
/**
|
|
34
|
+
* 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.
|
|
38
|
+
*/
|
|
39
|
+
runPacquet?: () => Promise<void>;
|
|
40
|
+
} & Partial<Pick<Config, 'ci' | 'sort' | 'strictDepBuilds' | 'workspaceConcurrency'>> & Required<Pick<Config, 'workspaceDir'>>;
|
|
32
41
|
export type CommandFullName = 'install' | 'add' | 'remove' | 'update' | 'import';
|
|
33
42
|
export declare function recursive(allProjects: Project[], params: string[], opts: RecursiveOptions, cmdFullName: CommandFullName): Promise<boolean | string>;
|
|
34
43
|
export declare function matchDependencies(match: (input: string) => string | null, manifest: ProjectManifest, include: IncludedDependencies): string[];
|
package/lib/recursive.js
CHANGED
|
@@ -18,6 +18,7 @@ import pLimit from 'p-limit';
|
|
|
18
18
|
import { getPinnedVersion } from './getPinnedVersion.js';
|
|
19
19
|
import { getSaveType } from './getSaveType.js';
|
|
20
20
|
import { handleIgnoredBuilds } from './handleIgnoredBuilds.js';
|
|
21
|
+
import { setupPolicyHandlers } from './policyHandlers.js';
|
|
21
22
|
import { createWorkspaceSpecs, updateToWorkspacePackagesFromManifest } from './updateWorkspaceDependencies.js';
|
|
22
23
|
export async function recursive(allProjects, params, opts, cmdFullName) {
|
|
23
24
|
if (allProjects.length === 0) {
|
|
@@ -33,6 +34,12 @@ export async function recursive(allProjects, params, opts, cmdFullName) {
|
|
|
33
34
|
const store = opts.storeControllerAndDir ?? await createStoreController(opts);
|
|
34
35
|
const workspacePackages = arrayOfWorkspacePackagesToMap(allProjects);
|
|
35
36
|
const targetDependenciesField = getSaveType(opts);
|
|
37
|
+
// See `installDeps.ts` for context; mirrored here so workspace-recursive
|
|
38
|
+
// installs also surface immature picks (loose-mode auto-persist or
|
|
39
|
+
// strict-mode prompt). The workspace manifest writer dedupes against the
|
|
40
|
+
// existing list, so a single drain at the end captures additions across
|
|
41
|
+
// every project.
|
|
42
|
+
const policyHandlers = setupPolicyHandlers(opts);
|
|
36
43
|
const installOpts = Object.assign(opts, {
|
|
37
44
|
allProjects: getAllProjects(manifestsByPath, opts.allProjectsGraph, opts.sort),
|
|
38
45
|
linkWorkspacePackagesDepth: opts.linkWorkspacePackages === 'deep' ? Infinity : opts.linkWorkspacePackages ? 0 : -1,
|
|
@@ -46,7 +53,9 @@ export async function recursive(allProjects, params, opts, cmdFullName) {
|
|
|
46
53
|
storeController: store.ctrl,
|
|
47
54
|
storeDir: store.dir,
|
|
48
55
|
targetDependenciesField,
|
|
56
|
+
resolutionVerifiers: store.resolutionVerifiers,
|
|
49
57
|
workspacePackages,
|
|
58
|
+
handleResolutionPolicyViolations: policyHandlers?.handleResolutionPolicyViolations,
|
|
50
59
|
});
|
|
51
60
|
const result = {};
|
|
52
61
|
const projectConfigRecord = createProjectConfigRecord(opts);
|
|
@@ -167,11 +176,17 @@ export async function recursive(allProjects, params, opts, cmdFullName) {
|
|
|
167
176
|
if ((mutatedImporters.length === 0) && cmdFullName === 'update' && opts.depth === 0) {
|
|
168
177
|
throw new PnpmError('NO_PACKAGE_IN_DEPENDENCIES', 'None of the specified packages were found in the dependencies of any of the projects.');
|
|
169
178
|
}
|
|
170
|
-
const { updatedCatalogs, updatedProjects: mutatedPkgs, ignoredBuilds, } = await mutateModules(mutatedImporters, {
|
|
179
|
+
const { updatedCatalogs, updatedProjects: mutatedPkgs, ignoredBuilds, resolutionPolicyViolations, } = await mutateModules(mutatedImporters, {
|
|
171
180
|
...installOpts,
|
|
172
181
|
storeController: store.ctrl,
|
|
182
|
+
resolutionVerifiers: store.resolutionVerifiers,
|
|
173
183
|
});
|
|
174
184
|
if (opts.save !== false) {
|
|
185
|
+
// Only pick entries when we'll actually persist. Otherwise the
|
|
186
|
+
// info log would claim entries were added that the workspace
|
|
187
|
+
// manifest never saw, and the next install would re-prompt or
|
|
188
|
+
// fail verification.
|
|
189
|
+
const policyUpdates = policyHandlers?.pickManifestUpdates(resolutionPolicyViolations);
|
|
175
190
|
const promises = mutatedPkgs.map(async ({ originalManifest, manifest, rootDir }) => {
|
|
176
191
|
return manifestsByPath[rootDir].writeProjectManifest(originalManifest ?? manifest);
|
|
177
192
|
});
|
|
@@ -179,6 +194,7 @@ export async function recursive(allProjects, params, opts, cmdFullName) {
|
|
|
179
194
|
updatedCatalogs,
|
|
180
195
|
cleanupUnusedCatalogs: opts.cleanupUnusedCatalogs,
|
|
181
196
|
allProjects,
|
|
197
|
+
...policyUpdates,
|
|
182
198
|
}));
|
|
183
199
|
await Promise.all(promises);
|
|
184
200
|
}
|
|
@@ -188,6 +204,10 @@ export async function recursive(allProjects, params, opts, cmdFullName) {
|
|
|
188
204
|
const pkgPaths = Object.keys(opts.selectedProjectsGraph).sort();
|
|
189
205
|
let updatedCatalogs;
|
|
190
206
|
const allIgnoredBuilds = new Set();
|
|
207
|
+
// Each per-project install returns its own slice of lockfile-resolution
|
|
208
|
+
// violations; accumulate them here so the post-loop persist step can
|
|
209
|
+
// dedup and write a single batch to the workspace manifest.
|
|
210
|
+
const allResolutionPolicyViolations = [];
|
|
191
211
|
const limitInstallation = pLimit(getWorkspaceConcurrency(opts.workspaceConcurrency));
|
|
192
212
|
await Promise.all(pkgPaths.map(async (rootDir) => limitInstallation(async () => {
|
|
193
213
|
const hooks = opts.ignorePnpmfile
|
|
@@ -239,6 +259,7 @@ export async function recursive(allProjects, params, opts, cmdFullName) {
|
|
|
239
259
|
updatedCatalogs: undefined, // there's no reason to add new or update catalogs on `pnpm remove`
|
|
240
260
|
updatedManifest: mutationResult.updatedProjects[0].manifest,
|
|
241
261
|
ignoredBuilds: mutationResult.ignoredBuilds,
|
|
262
|
+
resolutionPolicyViolations: mutationResult.resolutionPolicyViolations,
|
|
242
263
|
};
|
|
243
264
|
};
|
|
244
265
|
break;
|
|
@@ -249,7 +270,7 @@ export async function recursive(allProjects, params, opts, cmdFullName) {
|
|
|
249
270
|
break;
|
|
250
271
|
}
|
|
251
272
|
const localConfig = getProjectConfig(manifest) ?? {};
|
|
252
|
-
const { updatedCatalogs: newCatalogsAddition, updatedManifest: newManifest, ignoredBuilds, } = await action(manifest, {
|
|
273
|
+
const { updatedCatalogs: newCatalogsAddition, updatedManifest: newManifest, ignoredBuilds, resolutionPolicyViolations, } = await action(manifest, {
|
|
253
274
|
...installOpts,
|
|
254
275
|
...localConfig,
|
|
255
276
|
...opts.allProjectsGraph[rootDir]?.package,
|
|
@@ -263,6 +284,7 @@ export async function recursive(allProjects, params, opts, cmdFullName) {
|
|
|
263
284
|
}),
|
|
264
285
|
configByUri: installOpts.configByUri,
|
|
265
286
|
storeController: store.ctrl,
|
|
287
|
+
resolutionVerifiers: store.resolutionVerifiers,
|
|
266
288
|
});
|
|
267
289
|
if (opts.save !== false) {
|
|
268
290
|
await writeProjectManifest(newManifest);
|
|
@@ -276,6 +298,11 @@ export async function recursive(allProjects, params, opts, cmdFullName) {
|
|
|
276
298
|
allIgnoredBuilds.add(depPath);
|
|
277
299
|
}
|
|
278
300
|
}
|
|
301
|
+
if (resolutionPolicyViolations?.length) {
|
|
302
|
+
for (const violation of resolutionPolicyViolations) {
|
|
303
|
+
allResolutionPolicyViolations.push(violation);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
279
306
|
result[rootDir].status = 'passed';
|
|
280
307
|
}
|
|
281
308
|
catch (err) { // eslint-disable-line
|
|
@@ -294,11 +321,18 @@ export async function recursive(allProjects, params, opts, cmdFullName) {
|
|
|
294
321
|
}
|
|
295
322
|
})));
|
|
296
323
|
await handleIgnoredBuilds(opts, allIgnoredBuilds.size ? allIgnoredBuilds : undefined);
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
324
|
+
if (opts.save !== false) {
|
|
325
|
+
// Only pick entries when we'll actually persist. Otherwise the
|
|
326
|
+
// info log would claim entries were added that the workspace
|
|
327
|
+
// manifest never saw, mirroring the gate the shared-lockfile
|
|
328
|
+
// branch + installDeps already apply.
|
|
329
|
+
await updateWorkspaceManifest(opts.workspaceDir, {
|
|
330
|
+
updatedCatalogs,
|
|
331
|
+
cleanupUnusedCatalogs: opts.cleanupUnusedCatalogs,
|
|
332
|
+
allProjects,
|
|
333
|
+
...policyHandlers?.pickManifestUpdates(allResolutionPolicyViolations),
|
|
334
|
+
});
|
|
335
|
+
}
|
|
302
336
|
if (!opts.lockfileOnly && !opts.ignoreScripts && (cmdFullName === 'add' ||
|
|
303
337
|
cmdFullName === 'install' ||
|
|
304
338
|
cmdFullName === 'update')) {
|
package/lib/remove.js
CHANGED
|
@@ -135,6 +135,7 @@ export async function handler(opts, params) {
|
|
|
135
135
|
linkWorkspacePackagesDepth: opts.linkWorkspacePackages === 'deep' ? Infinity : opts.linkWorkspacePackages ? 0 : -1,
|
|
136
136
|
storeController: store.ctrl,
|
|
137
137
|
storeDir: store.dir,
|
|
138
|
+
resolutionVerifiers: store.resolutionVerifiers,
|
|
138
139
|
include,
|
|
139
140
|
});
|
|
140
141
|
const allProjects = opts.allProjects ?? (opts.workspaceDir
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type PolicyHandlersOptions, type PolicyViolation } from './policyHandlers.js';
|
|
2
|
+
export interface GlobalPolicyCallbacks {
|
|
3
|
+
handleResolutionPolicyViolations?: (violations: readonly PolicyViolation[]) => Promise<void>;
|
|
4
|
+
updateResolutionPolicyManifest?: (violations: readonly PolicyViolation[], dir: string) => Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export declare function createGlobalPolicyCallbacks(opts: PolicyHandlersOptions): GlobalPolicyCallbacks;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { updateWorkspaceManifest } from '@pnpm/workspace.workspace-manifest-writer';
|
|
2
|
+
import { setupPolicyHandlers, } from './policyHandlers.js';
|
|
3
|
+
export function createGlobalPolicyCallbacks(opts) {
|
|
4
|
+
const policyHandlers = setupPolicyHandlers(opts);
|
|
5
|
+
if (policyHandlers == null)
|
|
6
|
+
return {};
|
|
7
|
+
return {
|
|
8
|
+
handleResolutionPolicyViolations: policyHandlers.handleResolutionPolicyViolations,
|
|
9
|
+
updateResolutionPolicyManifest: async (violations, dir) => {
|
|
10
|
+
const policyUpdates = policyHandlers.pickManifestUpdates(violations);
|
|
11
|
+
if (policyUpdates != null) {
|
|
12
|
+
await updateWorkspaceManifest(dir, policyUpdates);
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=resolutionPolicyManifest.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface MakeRunPacquetOpts {
|
|
2
|
+
lockfileDir: string;
|
|
3
|
+
/**
|
|
4
|
+
* Which `configDependencies` entry installed pacquet: either the
|
|
5
|
+
* original unscoped `pacquet` or the official scoped
|
|
6
|
+
* `@pnpm/pacquet` mirror. Drives the directory we look in under
|
|
7
|
+
* `node_modules/.pnpm-config/<packageName>/`. Both packages ship
|
|
8
|
+
* the same shim and the same `@pacquet/<plat>-<arch>` binary
|
|
9
|
+
* sub-packages, so the rest of the lookup is identical.
|
|
10
|
+
*/
|
|
11
|
+
packageName: 'pacquet' | '@pnpm/pacquet';
|
|
12
|
+
/**
|
|
13
|
+
* The user's original `pnpm` argv (`process.argv.slice(2)`). Not
|
|
14
|
+
* forwarded to pacquet — we only inspect it to warn about flags
|
|
15
|
+
* pacquet won't see.
|
|
16
|
+
*/
|
|
17
|
+
argv: string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build the install-engine callback `mutateModules` invokes when
|
|
21
|
+
* `configDependencies` declares pacquet. Returns `undefined` when no
|
|
22
|
+
* pacquet binary is on disk — the caller falls back to the JS path in
|
|
23
|
+
* that case.
|
|
24
|
+
*
|
|
25
|
+
* The callback spawns the pacquet binary installed under
|
|
26
|
+
* `node_modules/.pnpm-config/pacquet` and forwards the user's own
|
|
27
|
+
* pnpm CLI flags. Pacquet's NDJSON stderr is parsed line-by-line and
|
|
28
|
+
* the valid JSON records are re-emitted on pnpm's global
|
|
29
|
+
* `streamParser` so `@pnpm/cli.default-reporter` renders pacquet's
|
|
30
|
+
* events the same way it renders pnpm's own. Non-JSON stderr lines
|
|
31
|
+
* (panic backtraces, unexpected diagnostics) are forwarded to the
|
|
32
|
+
* real stderr verbatim so they reach the user.
|
|
33
|
+
*/
|
|
34
|
+
/** Args the deps-installer passes per pacquet invocation. */
|
|
35
|
+
export interface RunPacquetCallOpts {
|
|
36
|
+
/**
|
|
37
|
+
* `true` when pnpm has already run a lockfileOnly resolve pass and
|
|
38
|
+
* the reporter has already accumulated one `pnpm:progress
|
|
39
|
+
* status:resolved` per package. Pacquet's own `resolved` events
|
|
40
|
+
* (emitted for wire-format parity as it walks the lockfile) are
|
|
41
|
+
* dropped on the way back through the reader so the reporter
|
|
42
|
+
* doesn't double-count. The frozen-install path passes `false`:
|
|
43
|
+
* pnpm did no resolution there, so pacquet's events are the only
|
|
44
|
+
* source.
|
|
45
|
+
*/
|
|
46
|
+
filterResolvedProgress?: boolean;
|
|
47
|
+
}
|
|
48
|
+
export declare function makeRunPacquet(opts: MakeRunPacquetOpts): (callOpts?: RunPacquetCallOpts) => Promise<void>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import readline from 'node:readline';
|
|
6
|
+
import { PnpmError } from '@pnpm/error';
|
|
7
|
+
import { logger, streamParser } from '@pnpm/logger';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
// The runtime `streamParser` is a `Transform` stream (split2 + JSON.parse).
|
|
10
|
+
// Its public typing only exposes `on`/`removeListener`, so we narrow to the
|
|
11
|
+
// writable side here to feed pacquet's NDJSON lines back through the same
|
|
12
|
+
// parser that `@pnpm/cli.default-reporter` listens on.
|
|
13
|
+
const streamParserWritable = streamParser;
|
|
14
|
+
export function makeRunPacquet(opts) {
|
|
15
|
+
return async (callOpts) => {
|
|
16
|
+
const pacquetBin = resolvePacquetBin(opts.lockfileDir, opts.packageName);
|
|
17
|
+
// Always the same fixed args. We don't forward pnpm's CLI flags
|
|
18
|
+
// even though pacquet's `install` subcommand mirrors most of them:
|
|
19
|
+
// pnpm has commands like `add` and `update` that carry flags
|
|
20
|
+
// pacquet's `install` doesn't recognize (e.g., `--save-dev`,
|
|
21
|
+
// `--save-peer`), and clap would reject them. The settings users
|
|
22
|
+
// care about live in `pnpm-workspace.yaml` / `.npmrc`, which
|
|
23
|
+
// pacquet reads on its own.
|
|
24
|
+
const args = ['--reporter=ndjson', 'install', '--frozen-lockfile'];
|
|
25
|
+
const droppedFlags = collectDroppedFlags(opts.argv);
|
|
26
|
+
if (droppedFlags.length > 0) {
|
|
27
|
+
logger.warn({
|
|
28
|
+
message: `The following CLI flags are not forwarded to pacquet and may not be honored: ${droppedFlags.join(' ')}. Move the equivalent settings into pnpm-workspace.yaml (or .npmrc for auth/registry) if pacquet needs them.`,
|
|
29
|
+
prefix: opts.lockfileDir,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
// Banner so users can tell at a glance their install is going
|
|
33
|
+
// through the Rust engine rather than the JS path. Chalk is the
|
|
34
|
+
// same dependency the default reporter uses for the "+ pkg
|
|
35
|
+
// version" summary, so colorization respects the user's TTY
|
|
36
|
+
// settings consistently.
|
|
37
|
+
const banner = [
|
|
38
|
+
chalk.magentaBright('▶ Using pacquet for this install'),
|
|
39
|
+
chalk.gray(' pacquet is pnpm\'s Rust install engine (preview); declared in configDependencies.'),
|
|
40
|
+
].join('\n');
|
|
41
|
+
logger.info({ message: banner, prefix: opts.lockfileDir });
|
|
42
|
+
const child = spawn(pacquetBin, args, {
|
|
43
|
+
cwd: opts.lockfileDir,
|
|
44
|
+
stdio: ['ignore', 'inherit', 'pipe'],
|
|
45
|
+
});
|
|
46
|
+
const filterResolved = callOpts?.filterResolvedProgress === true;
|
|
47
|
+
const rl = readline.createInterface({ input: child.stderr, crlfDelay: Infinity });
|
|
48
|
+
rl.on('line', (line) => {
|
|
49
|
+
if (!line)
|
|
50
|
+
return;
|
|
51
|
+
let parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = JSON.parse(line);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
process.stderr.write(`${line}\n`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (filterResolved &&
|
|
60
|
+
typeof parsed === 'object' && parsed !== null &&
|
|
61
|
+
parsed.name === 'pnpm:progress' &&
|
|
62
|
+
parsed.status === 'resolved') {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
streamParserWritable.write(`${line}\n`);
|
|
66
|
+
});
|
|
67
|
+
await new Promise((resolve, reject) => {
|
|
68
|
+
child.once('error', reject);
|
|
69
|
+
child.once('close', (code) => {
|
|
70
|
+
rl.close();
|
|
71
|
+
if (code === 0) {
|
|
72
|
+
resolve();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
reject(new PnpmError('PACQUET_INSTALL_FAILED', `pacquet exited with code ${code ?? 'null'}`));
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Path of the platform-specific native pacquet binary for the host. The
|
|
82
|
+
* pacquet npm package ships a Node wrapper at `bin/pacquet` that uses
|
|
83
|
+
* `require.resolve('@pacquet/<platform>-<arch>/pacquet[.exe]')` to find
|
|
84
|
+
* the binary — so the platform package lands as a *sibling* of pacquet,
|
|
85
|
+
* not inside its own `node_modules` (pacquet's own `node_modules` is
|
|
86
|
+
* empty after configDependencies install). Use Node's resolver rooted
|
|
87
|
+
* at pacquet's own `package.json` so we follow the same path the
|
|
88
|
+
* wrapper would have.
|
|
89
|
+
*
|
|
90
|
+
* The `realpathSync` is required: `.pnpm-config/pacquet` is a symlink
|
|
91
|
+
* into the global virtual store, and Node's `createRequire` builds its
|
|
92
|
+
* search paths from the *literal* ancestors of the path it's given —
|
|
93
|
+
* it won't follow the symlink up into the store dir where the
|
|
94
|
+
* `@pacquet/<plat>-<arch>` sibling actually lives.
|
|
95
|
+
*/
|
|
96
|
+
function resolvePacquetBin(lockfileDir, packageName) {
|
|
97
|
+
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
98
|
+
const pacquetPkg = fs.realpathSync(path.join(lockfileDir, 'node_modules/.pnpm-config', packageName, 'package.json'));
|
|
99
|
+
return createRequire(pacquetPkg).resolve(`@pacquet/${process.platform}-${process.arch}/pacquet${ext}`);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Pull the CLI flags out of pnpm's argv so we can warn about them
|
|
103
|
+
* before pacquet runs. We don't forward any of them — pacquet always
|
|
104
|
+
* gets `install --frozen-lockfile --reporter=ndjson` — but most are
|
|
105
|
+
* handled by pnpm itself before delegation (`--save-dev` rewrites
|
|
106
|
+
* `package.json`, `--filter` selects projects, etc.) so listing them
|
|
107
|
+
* to the user makes the "not forwarded" surface concrete.
|
|
108
|
+
*
|
|
109
|
+
* Flags we explicitly emit ourselves (`--frozen-lockfile`,
|
|
110
|
+
* `--reporter=ndjson`) are filtered out: they're honored, so warning
|
|
111
|
+
* about them would be misleading. `--config.*` is filtered too —
|
|
112
|
+
* those configure pnpm's runtime and aren't intended for the install
|
|
113
|
+
* engine.
|
|
114
|
+
*/
|
|
115
|
+
function collectDroppedFlags(argv) {
|
|
116
|
+
return argv.filter((arg) => {
|
|
117
|
+
if (!arg.startsWith('-'))
|
|
118
|
+
return false;
|
|
119
|
+
if (arg === '--frozen-lockfile' || arg === '--reporter=ndjson')
|
|
120
|
+
return false;
|
|
121
|
+
if (arg.startsWith('--config.'))
|
|
122
|
+
return false;
|
|
123
|
+
return true;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=runPacquet.js.map
|
package/lib/update/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { pick, pluck, unnest } from 'ramda';
|
|
|
12
12
|
import { renderHelp } from 'render-help';
|
|
13
13
|
import { installDeps } from '../installDeps.js';
|
|
14
14
|
import { parseUpdateParam } from '../recursive.js';
|
|
15
|
+
import { createGlobalPolicyCallbacks } from '../resolutionPolicyManifest.js';
|
|
15
16
|
import { getUpdateChoices } from './getUpdateChoices.js';
|
|
16
17
|
export function rcOptionsTypes() {
|
|
17
18
|
return pick([
|
|
@@ -151,7 +152,10 @@ export async function handler(opts, params = [], commands) {
|
|
|
151
152
|
hint: 'Run "pnpm setup" to create it automatically, or set the global-bin-dir setting, or the PNPM_HOME env variable. The global bin directory should be in the PATH.',
|
|
152
153
|
});
|
|
153
154
|
}
|
|
154
|
-
return handleGlobalUpdate(
|
|
155
|
+
return handleGlobalUpdate({
|
|
156
|
+
...opts,
|
|
157
|
+
...createGlobalPolicyCallbacks(opts),
|
|
158
|
+
}, params, commands ?? {});
|
|
155
159
|
}
|
|
156
160
|
const rebuildHandler = commands?.rebuild;
|
|
157
161
|
if (opts.interactive) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/installing.commands",
|
|
3
|
-
"version": "1100.
|
|
3
|
+
"version": "1100.4.0",
|
|
4
4
|
"description": "Commands for installation",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pnpm",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"@zkochan/rimraf": "^4.0.0",
|
|
34
34
|
"@zkochan/table": "^2.0.1",
|
|
35
35
|
"chalk": "^5.6.0",
|
|
36
|
+
"ci-info": "^4.3.0",
|
|
36
37
|
"enquirer": "^2.4.1",
|
|
37
38
|
"get-npm-tarball-url": "^2.1.0",
|
|
38
39
|
"is-subdir": "^2.0.0",
|
|
@@ -43,45 +44,46 @@
|
|
|
43
44
|
"ramda": "npm:@pnpm/ramda@0.28.1",
|
|
44
45
|
"render-help": "^2.0.0",
|
|
45
46
|
"version-selector-type": "^3.0.0",
|
|
46
|
-
"@pnpm/building.after-install": "1101.0.
|
|
47
|
+
"@pnpm/building.after-install": "1101.0.14",
|
|
47
48
|
"@pnpm/catalogs.types": "1100.0.0",
|
|
48
|
-
"@pnpm/cli.command": "1100.0.1",
|
|
49
49
|
"@pnpm/cli.common-cli-options-help": "1100.0.1",
|
|
50
|
-
"@pnpm/cli.utils": "1101.0.
|
|
50
|
+
"@pnpm/cli.utils": "1101.0.6",
|
|
51
|
+
"@pnpm/config.pick-registry-for-package": "1100.0.4",
|
|
51
52
|
"@pnpm/config.matcher": "1100.0.1",
|
|
52
|
-
"@pnpm/config.
|
|
53
|
-
"@pnpm/
|
|
53
|
+
"@pnpm/config.reader": "1101.3.3",
|
|
54
|
+
"@pnpm/cli.command": "1100.0.1",
|
|
55
|
+
"@pnpm/config.writer": "1100.0.9",
|
|
54
56
|
"@pnpm/constants": "1100.0.0",
|
|
55
|
-
"@pnpm/deps.inspection.outdated": "1100.0
|
|
56
|
-
"@pnpm/deps.path": "1100.0.
|
|
57
|
-
"@pnpm/deps.status": "1100.0.
|
|
58
|
-
"@pnpm/fs.graceful-fs": "1100.1.0",
|
|
57
|
+
"@pnpm/deps.inspection.outdated": "1100.1.0",
|
|
58
|
+
"@pnpm/deps.path": "1100.0.4",
|
|
59
|
+
"@pnpm/deps.status": "1100.0.17",
|
|
59
60
|
"@pnpm/error": "1100.0.0",
|
|
61
|
+
"@pnpm/fs.graceful-fs": "1100.1.0",
|
|
60
62
|
"@pnpm/fs.read-modules-dir": "1100.0.1",
|
|
61
|
-
"@pnpm/global.commands": "1100.0.
|
|
62
|
-
"@pnpm/
|
|
63
|
-
"@pnpm/
|
|
64
|
-
"@pnpm/installing.dedupe.check": "1100.0.
|
|
65
|
-
"@pnpm/installing.env-installer": "1101.0
|
|
66
|
-
"@pnpm/installing.deps-installer": "1101.
|
|
67
|
-
"@pnpm/lockfile.types": "1100.0.
|
|
68
|
-
"@pnpm/pkg-manifest.
|
|
69
|
-
"@pnpm/pkg-manifest.
|
|
63
|
+
"@pnpm/global.commands": "1100.0.19",
|
|
64
|
+
"@pnpm/hooks.pnpmfile": "1100.0.10",
|
|
65
|
+
"@pnpm/installing.context": "1100.0.12",
|
|
66
|
+
"@pnpm/installing.dedupe.check": "1100.0.7",
|
|
67
|
+
"@pnpm/installing.env-installer": "1101.1.0",
|
|
68
|
+
"@pnpm/installing.deps-installer": "1101.3.0",
|
|
69
|
+
"@pnpm/lockfile.types": "1100.0.7",
|
|
70
|
+
"@pnpm/pkg-manifest.utils": "1100.2.0",
|
|
71
|
+
"@pnpm/pkg-manifest.reader": "1100.0.4",
|
|
70
72
|
"@pnpm/resolving.parse-wanted-dependency": "1100.0.1",
|
|
71
|
-
"@pnpm/resolving.resolver-base": "1100.
|
|
72
|
-
"@pnpm/store.
|
|
73
|
-
"@pnpm/
|
|
74
|
-
"@pnpm/
|
|
75
|
-
"@pnpm/
|
|
76
|
-
"@pnpm/workspace.
|
|
77
|
-
"@pnpm/
|
|
78
|
-
"@pnpm/workspace.
|
|
79
|
-
"@pnpm/workspace.
|
|
80
|
-
"@pnpm/workspace.
|
|
81
|
-
"@pnpm/workspace.
|
|
82
|
-
"@pnpm/
|
|
83
|
-
"@pnpm/workspace.
|
|
84
|
-
"@pnpm/workspace.
|
|
73
|
+
"@pnpm/resolving.resolver-base": "1100.3.0",
|
|
74
|
+
"@pnpm/store.connection-manager": "1100.2.1",
|
|
75
|
+
"@pnpm/resolving.npm-resolver": "1101.3.0",
|
|
76
|
+
"@pnpm/store.controller": "1101.0.8",
|
|
77
|
+
"@pnpm/types": "1101.1.1",
|
|
78
|
+
"@pnpm/workspace.projects-filter": "1100.0.13",
|
|
79
|
+
"@pnpm/workspace.projects-graph": "1100.0.10",
|
|
80
|
+
"@pnpm/workspace.project-manifest-reader": "1100.0.7",
|
|
81
|
+
"@pnpm/workspace.projects-sorter": "1100.0.3",
|
|
82
|
+
"@pnpm/workspace.projects-reader": "1101.0.6",
|
|
83
|
+
"@pnpm/workspace.root-finder": "1100.0.1",
|
|
84
|
+
"@pnpm/workspace.project-manifest-writer": "1100.0.4",
|
|
85
|
+
"@pnpm/workspace.state": "1100.0.14",
|
|
86
|
+
"@pnpm/workspace.workspace-manifest-writer": "1100.0.9"
|
|
85
87
|
},
|
|
86
88
|
"peerDependencies": {
|
|
87
89
|
"@pnpm/logger": ">=1001.0.0 <1002.0.0"
|
|
@@ -94,7 +96,6 @@
|
|
|
94
96
|
"@types/ramda": "0.31.1",
|
|
95
97
|
"@types/yarnpkg__lockfile": "^1.1.9",
|
|
96
98
|
"@types/zkochan__table": "npm:@types/table@6.0.0",
|
|
97
|
-
"ci-info": "^4.3.0",
|
|
98
99
|
"delay": "^7.0.0",
|
|
99
100
|
"jest-diff": "^30.3.0",
|
|
100
101
|
"path-name": "^1.0.0",
|
|
@@ -105,18 +106,18 @@
|
|
|
105
106
|
"write-json-file": "^7.0.0",
|
|
106
107
|
"write-package": "7.2.0",
|
|
107
108
|
"write-yaml-file": "^6.0.0",
|
|
108
|
-
"@pnpm/
|
|
109
|
-
"@pnpm/
|
|
110
|
-
"@pnpm/
|
|
111
|
-
"@pnpm/
|
|
109
|
+
"@pnpm/assert-project": "1100.0.10",
|
|
110
|
+
"@pnpm/installing.commands": "1100.4.0",
|
|
111
|
+
"@pnpm/installing.modules-yaml": "1100.0.5",
|
|
112
|
+
"@pnpm/prepare": "1100.0.10",
|
|
112
113
|
"@pnpm/logger": "1100.0.0",
|
|
113
114
|
"@pnpm/test-fixtures": "1100.0.0",
|
|
115
|
+
"@pnpm/store.index": "1100.1.0",
|
|
114
116
|
"@pnpm/test-ipc-server": "1100.0.0",
|
|
115
|
-
"@pnpm/
|
|
117
|
+
"@pnpm/testing.mock-agent": "1100.0.6",
|
|
116
118
|
"@pnpm/testing.command-defaults": "1100.0.1",
|
|
117
|
-
"@pnpm/
|
|
118
|
-
"@pnpm/
|
|
119
|
-
"@pnpm/workspace.projects-filter": "1100.0.11"
|
|
119
|
+
"@pnpm/worker": "1100.1.7",
|
|
120
|
+
"@pnpm/workspace.projects-filter": "1100.0.13"
|
|
120
121
|
},
|
|
121
122
|
"engines": {
|
|
122
123
|
"node": ">=22.13"
|