@pnpm/installing.deps-installer 1101.1.2 → 1101.3.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/install/extendInstallOptions.d.ts +52 -1
- package/lib/install/extendInstallOptions.js +1 -0
- package/lib/install/index.d.ts +14 -1
- package/lib/install/index.js +201 -21
- package/lib/install/link.js +7 -1
- package/lib/install/recordLockfileVerified.d.ts +22 -0
- package/lib/install/recordLockfileVerified.js +24 -0
- package/lib/install/verifyLockfileResolutions.d.ts +59 -0
- package/lib/install/verifyLockfileResolutions.js +225 -0
- package/lib/install/verifyLockfileResolutionsCache.d.ts +80 -0
- package/lib/install/verifyLockfileResolutionsCache.js +335 -0
- package/lib/install/writeLockfilesAndRecordVerified.d.ts +14 -0
- package/lib/install/writeLockfilesAndRecordVerified.js +32 -0
- package/lib/install/writeWantedLockfileAndRecordVerified.d.ts +12 -0
- package/lib/install/writeWantedLockfileAndRecordVerified.js +28 -0
- package/package.json +53 -53
|
@@ -5,7 +5,7 @@ import type { ProjectOptions } from '@pnpm/installing.context';
|
|
|
5
5
|
import type { HoistingLimits } from '@pnpm/installing.deps-restorer';
|
|
6
6
|
import type { IncludedDependencies } from '@pnpm/installing.modules-yaml';
|
|
7
7
|
import type { LockfileObject } from '@pnpm/lockfile.fs';
|
|
8
|
-
import type { WorkspacePackages } from '@pnpm/resolving.resolver-base';
|
|
8
|
+
import type { ResolutionPolicyViolation, ResolutionVerifier, WorkspacePackages } from '@pnpm/resolving.resolver-base';
|
|
9
9
|
import type { StoreController } from '@pnpm/store.controller-types';
|
|
10
10
|
import type { AllowedDeprecatedVersions, PackageExtension, PackageVulnerabilityAudit, PeerDependencyRules, ReadPackageHook, Registries, RegistryConfig, SupportedArchitectures, TrustPolicy } from '@pnpm/types';
|
|
11
11
|
import type { ReporterFunction } from '../types.js';
|
|
@@ -148,11 +148,62 @@ export interface StrictInstallOptions {
|
|
|
148
148
|
ci?: boolean;
|
|
149
149
|
minimumReleaseAge?: number;
|
|
150
150
|
minimumReleaseAgeExclude?: string[];
|
|
151
|
+
/**
|
|
152
|
+
* Resolver-agnostic post-tree gate, invoked between
|
|
153
|
+
* `resolveDependencyTree` and `resolvePeers` inside
|
|
154
|
+
* `resolveDependencies`. Receives the violations the verifier
|
|
155
|
+
* fan-out collected from the freshly-resolved tree. Throwing here
|
|
156
|
+
* unwinds the install before peer-dep resolution runs — nothing on
|
|
157
|
+
* disk has changed, and the (potentially expensive) peer pass is
|
|
158
|
+
* skipped on abort.
|
|
159
|
+
*
|
|
160
|
+
* Intentionally policy-neutral. Each verifier owns its violation
|
|
161
|
+
* codes (`MINIMUM_RELEASE_AGE_VIOLATION`, `TRUST_DOWNGRADE`, …); the
|
|
162
|
+
* install command filters by code to decide what to do. Future
|
|
163
|
+
* resolvers can plug verifiers in without touching this signature.
|
|
164
|
+
*/
|
|
165
|
+
handleResolutionPolicyViolations?: (violations: readonly ResolutionPolicyViolation[]) => Promise<void>;
|
|
166
|
+
/**
|
|
167
|
+
* Resolver-side verifiers that re-check each lockfile-pinned resolution
|
|
168
|
+
* against policies configured upstream (today: at most one,
|
|
169
|
+
* `npm.minimumReleaseAge` in strict mode). Constructed by `createClient`
|
|
170
|
+
* and surfaced via the `createStoreController` return; mutateModules
|
|
171
|
+
* fans out across the list once, right after the lockfile is loaded
|
|
172
|
+
* from disk. Empty when no policy is active.
|
|
173
|
+
*/
|
|
174
|
+
resolutionVerifiers: ResolutionVerifier[];
|
|
175
|
+
/**
|
|
176
|
+
* pnpm's on-disk cache directory. When set together with non-empty
|
|
177
|
+
* `resolutionVerifiers`, the lockfile verification result is memoized
|
|
178
|
+
* in `<cacheDir>/lockfile-verified.jsonl` so repeat installs against an
|
|
179
|
+
* unchanged lockfile skip the per-package registry round trip. The
|
|
180
|
+
* record is policy-neutral; each active resolver-side verifier writes
|
|
181
|
+
* its own slot under `verifiers[<key>]`.
|
|
182
|
+
*/
|
|
183
|
+
cacheDir?: string;
|
|
151
184
|
trustPolicy?: TrustPolicy;
|
|
152
185
|
trustPolicyExclude?: string[];
|
|
153
186
|
trustPolicyIgnoreAfter?: number;
|
|
154
187
|
packageVulnerabilityAudit?: PackageVulnerabilityAudit;
|
|
155
188
|
blockExoticSubdeps?: boolean;
|
|
189
|
+
/**
|
|
190
|
+
* Optional alternative install engine. When set, the frozen-install
|
|
191
|
+
* path invokes this callback instead of `headlessInstall`. The CLI
|
|
192
|
+
* layer constructs it (today: spawning the pacquet binary installed
|
|
193
|
+
* via `configDependencies` and forwarding pnpm's own CLI argv); the
|
|
194
|
+
* installer treats it as an opaque "do the install" hook so it
|
|
195
|
+
* doesn't need to know about pacquet's binary path, CLI surface, or
|
|
196
|
+
* any settings that only pacquet consumes.
|
|
197
|
+
*
|
|
198
|
+
* `filterResolvedProgress` tells the helper to drop the engine's
|
|
199
|
+
* own `pnpm:progress status:resolved` events because pnpm already
|
|
200
|
+
* emitted one per package during a preceding lockfileOnly resolve
|
|
201
|
+
* pass. The frozen-install path passes `false` (or nothing): no
|
|
202
|
+
* resolve pass ran, so the engine's events are the only source.
|
|
203
|
+
*/
|
|
204
|
+
runPacquet?: (opts?: {
|
|
205
|
+
filterResolvedProgress?: boolean;
|
|
206
|
+
}) => Promise<void>;
|
|
156
207
|
/**
|
|
157
208
|
* If true, `mutateModules` does not emit the per-install `summary` log
|
|
158
209
|
* event. Used by `pnpm add -g` when it runs multiple isolated installs
|
package/lib/install/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Catalogs } from '@pnpm/catalogs.types';
|
|
|
2
2
|
import { PnpmError } from '@pnpm/error';
|
|
3
3
|
import { type PinnedVersion, type UpdateMatchingFunction, type WantedDependency } from '@pnpm/installing.deps-resolver';
|
|
4
4
|
import { type InstallationResultStats } from '@pnpm/installing.deps-restorer';
|
|
5
|
-
import type { PreferredVersions } from '@pnpm/resolving.resolver-base';
|
|
5
|
+
import type { PreferredVersions, ResolutionPolicyViolation } from '@pnpm/resolving.resolver-base';
|
|
6
6
|
import type { DependenciesField, DepPath, IgnoredBuilds, PeerDependencyIssues, ProjectId, ProjectManifest, ProjectRootDir, ReadPackageHook } from '@pnpm/types';
|
|
7
7
|
import { type InstallOptions } from './extendInstallOptions.js';
|
|
8
8
|
interface InstallMutationOptions {
|
|
@@ -45,6 +45,8 @@ export interface InstallResult {
|
|
|
45
45
|
updatedCatalogs: Catalogs | undefined;
|
|
46
46
|
updatedManifest: ProjectManifest;
|
|
47
47
|
ignoredBuilds: IgnoredBuilds | undefined;
|
|
48
|
+
/** Forwarded from {@link MutateModulesResult.resolutionPolicyViolations}. */
|
|
49
|
+
resolutionPolicyViolations: ResolutionPolicyViolation[];
|
|
48
50
|
}
|
|
49
51
|
export declare function install(manifest: ProjectManifest, opts: Opts): Promise<InstallResult>;
|
|
50
52
|
export type MutatedProject = DependenciesMutation & {
|
|
@@ -60,6 +62,8 @@ export interface MutateModulesInSingleProjectResult {
|
|
|
60
62
|
updatedCatalogs: Catalogs | undefined;
|
|
61
63
|
updatedProject: UpdatedProject;
|
|
62
64
|
ignoredBuilds: IgnoredBuilds | undefined;
|
|
65
|
+
/** Forwarded from {@link MutateModulesResult.resolutionPolicyViolations}. */
|
|
66
|
+
resolutionPolicyViolations: ResolutionPolicyViolation[];
|
|
63
67
|
}
|
|
64
68
|
export declare function mutateModulesInSingleProject(project: MutatedProject & {
|
|
65
69
|
binsDir?: string;
|
|
@@ -73,6 +77,15 @@ export interface MutateModulesResult {
|
|
|
73
77
|
stats: InstallationResultStats;
|
|
74
78
|
depsRequiringBuild?: DepPath[];
|
|
75
79
|
ignoredBuilds: IgnoredBuilds | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* Resolver-policy violations the post-resolution scan found in the
|
|
82
|
+
* freshly-resolved lockfile. Each violation carries a verifier code
|
|
83
|
+
* (e.g. `MINIMUM_RELEASE_AGE_VIOLATION`, `TRUST_DOWNGRADE`); the
|
|
84
|
+
* install command filters by code to decide what to do (persist to
|
|
85
|
+
* `minimumReleaseAgeExclude`, log, etc.). Empty array when no
|
|
86
|
+
* verifier reported a violation or no policy was active.
|
|
87
|
+
*/
|
|
88
|
+
resolutionPolicyViolations: ResolutionPolicyViolation[];
|
|
76
89
|
}
|
|
77
90
|
export declare function mutateModules(projects: MutatedProject[], maybeOpts: MutateModulesOptions): Promise<MutateModulesResult>;
|
|
78
91
|
export declare function addDependenciesToPackage(manifest: ProjectManifest, dependencySelectors: string[], opts: Omit<InstallOptions, 'allProjects'> & {
|
package/lib/install/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { getContext } from '@pnpm/installing.context';
|
|
|
15
15
|
import { getWantedDependencies, resolveDependencies, } from '@pnpm/installing.deps-resolver';
|
|
16
16
|
import { extendProjectsWithTargetDirs, headlessInstall } from '@pnpm/installing.deps-restorer';
|
|
17
17
|
import { writeModulesManifest } from '@pnpm/installing.modules-yaml';
|
|
18
|
-
import { cleanGitBranchLockfiles, readWantedLockfile, writeCurrentLockfile, writeLockfiles, writeWantedLockfile, } from '@pnpm/lockfile.fs';
|
|
18
|
+
import { cleanGitBranchLockfiles, getWantedLockfileName, readWantedLockfile, writeCurrentLockfile, writeLockfiles, writeWantedLockfile, } from '@pnpm/lockfile.fs';
|
|
19
19
|
import { getPreferredVersionsFromLockfileAndManifests } from '@pnpm/lockfile.preferred-versions';
|
|
20
20
|
import { calcPatchHashes, createOverridesMapFromParsed, getOutdatedLockfileSetting, } from '@pnpm/lockfile.settings-checker';
|
|
21
21
|
import { writePnpFile } from '@pnpm/lockfile.to-pnp';
|
|
@@ -38,6 +38,9 @@ import { extendOptions, } from './extendInstallOptions.js';
|
|
|
38
38
|
import { linkPackages } from './link.js';
|
|
39
39
|
import { reportPeerDependencyIssues } from './reportPeerDependencyIssues.js';
|
|
40
40
|
import { validateModules } from './validateModules.js';
|
|
41
|
+
import { verifyLockfileResolutions } from './verifyLockfileResolutions.js';
|
|
42
|
+
import { writeLockfilesAndRecordVerified } from './writeLockfilesAndRecordVerified.js';
|
|
43
|
+
import { writeWantedLockfileAndRecordVerified } from './writeWantedLockfileAndRecordVerified.js';
|
|
41
44
|
class LockfileConfigMismatchError extends PnpmError {
|
|
42
45
|
constructor(outdatedLockfileSettingName) {
|
|
43
46
|
super('LOCKFILE_CONFIG_MISMATCH', `Cannot proceed with the frozen installation. The current "${outdatedLockfileSettingName}" configuration doesn't match the value found in the lockfile`, {
|
|
@@ -57,7 +60,7 @@ export async function install(manifest, opts) {
|
|
|
57
60
|
if (opts.agent) {
|
|
58
61
|
return installFromPnpmRegistry(manifest, rootDir, opts);
|
|
59
62
|
}
|
|
60
|
-
const { updatedCatalogs, updatedProjects: projects, ignoredBuilds } = await mutateModules([
|
|
63
|
+
const { updatedCatalogs, updatedProjects: projects, ignoredBuilds, resolutionPolicyViolations } = await mutateModules([
|
|
61
64
|
{
|
|
62
65
|
mutation: 'install',
|
|
63
66
|
pruneDirectDependencies: opts.pruneDirectDependencies,
|
|
@@ -76,7 +79,7 @@ export async function install(manifest, opts) {
|
|
|
76
79
|
binsDir: opts.binsDir,
|
|
77
80
|
}],
|
|
78
81
|
});
|
|
79
|
-
return { updatedCatalogs, updatedManifest: projects[0].manifest, ignoredBuilds };
|
|
82
|
+
return { updatedCatalogs, updatedManifest: projects[0].manifest, ignoredBuilds, resolutionPolicyViolations };
|
|
80
83
|
}
|
|
81
84
|
export async function mutateModulesInSingleProject(project, maybeOpts) {
|
|
82
85
|
const result = await mutateModules([
|
|
@@ -98,6 +101,7 @@ export async function mutateModulesInSingleProject(project, maybeOpts) {
|
|
|
98
101
|
updatedCatalogs: result.updatedCatalogs,
|
|
99
102
|
updatedProject: result.updatedProjects[0],
|
|
100
103
|
ignoredBuilds: result.ignoredBuilds,
|
|
104
|
+
resolutionPolicyViolations: result.resolutionPolicyViolations,
|
|
101
105
|
};
|
|
102
106
|
}
|
|
103
107
|
const pickCatalogSpecifier = {
|
|
@@ -107,6 +111,11 @@ const pickCatalogSpecifier = {
|
|
|
107
111
|
};
|
|
108
112
|
export async function mutateModules(projects, maybeOpts) {
|
|
109
113
|
const reporter = maybeOpts?.reporter;
|
|
114
|
+
const detachReporter = (reporter != null) && typeof reporter === 'function'
|
|
115
|
+
? () => {
|
|
116
|
+
streamParser.removeListener('data', reporter);
|
|
117
|
+
}
|
|
118
|
+
: () => { };
|
|
110
119
|
if ((reporter != null) && typeof reporter === 'function') {
|
|
111
120
|
streamParser.on('data', reporter);
|
|
112
121
|
}
|
|
@@ -153,6 +162,60 @@ export async function mutateModules(projects, maybeOpts) {
|
|
|
153
162
|
ctx = await getContext(opts);
|
|
154
163
|
}
|
|
155
164
|
}
|
|
165
|
+
// Re-validate every entry in the lockfile against the policies the
|
|
166
|
+
// resolver chain was built with (today: minimumReleaseAge in strict mode
|
|
167
|
+
// via the npm verifier; the abstraction supports other resolvers
|
|
168
|
+
// attaching their own verifiers). The threat model is a lockfile that
|
|
169
|
+
// someone else resolved — committed to the repo, restored from a CI
|
|
170
|
+
// cache, etc. — bypassing the local resolver's policy filters; the local
|
|
171
|
+
// resolver's own filters already cover fresh resolution. We run this
|
|
172
|
+
// exactly once, right after the lockfile is loaded from disk, before any
|
|
173
|
+
// path branches.
|
|
174
|
+
//
|
|
175
|
+
// Skipped when we already know pacquet will run the install: pacquet's
|
|
176
|
+
// frozen-install path applies the same resolver-policy gate (port of
|
|
177
|
+
// this function), so re-running here would duplicate the work — and
|
|
178
|
+
// for `minimumReleaseAge` in strict mode each lockfile entry is an
|
|
179
|
+
// HTTP probe.
|
|
180
|
+
//
|
|
181
|
+
// The predicate mirrors every short-circuit `tryFrozenInstall` checks
|
|
182
|
+
// before reaching the pacquet branch: anything that would make it
|
|
183
|
+
// return null, throw, or fall through to the JS path must keep
|
|
184
|
+
// verification on. The optimistic `preferFrozenLockfile` path decides
|
|
185
|
+
// whether to delegate later (based on `allProjectsAreUpToDate`), which
|
|
186
|
+
// isn't known here — so verification still runs in that window, the
|
|
187
|
+
// duplicate is bounded to it.
|
|
188
|
+
const willDelegateToPacquet = opts.runPacquet != null &&
|
|
189
|
+
installsOnly &&
|
|
190
|
+
!opts.lockfileOnly &&
|
|
191
|
+
!opts.fixLockfile &&
|
|
192
|
+
!opts.dedupe &&
|
|
193
|
+
!ctx.lockfileHadConflicts &&
|
|
194
|
+
ctx.existsNonEmptyWantedLockfile &&
|
|
195
|
+
(opts.frozenLockfile === true || opts.frozenLockfileIfExists === true);
|
|
196
|
+
if (!willDelegateToPacquet) {
|
|
197
|
+
const cacheActive = opts.cacheDir != null && opts.resolutionVerifiers.length > 0;
|
|
198
|
+
const wantedLockfilePath = cacheActive
|
|
199
|
+
? path.resolve(ctx.lockfileDir, await getWantedLockfileName({
|
|
200
|
+
useGitBranchLockfile: opts.useGitBranchLockfile,
|
|
201
|
+
mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
|
|
202
|
+
}))
|
|
203
|
+
: undefined;
|
|
204
|
+
try {
|
|
205
|
+
await verifyLockfileResolutions(ctx.wantedLockfile, opts.resolutionVerifiers, {
|
|
206
|
+
cacheDir: opts.cacheDir,
|
|
207
|
+
lockfilePath: wantedLockfilePath,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
// verifyLockfileResolutions is the one throw site in this function
|
|
212
|
+
// that's part of normal user-facing operation (a rejected lockfile);
|
|
213
|
+
// other throws here are unexpected. Detach the reporter listener so
|
|
214
|
+
// long-lived processes don't leak it on every rejected install.
|
|
215
|
+
detachReporter();
|
|
216
|
+
throw err;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
156
219
|
if (opts.hooks.preResolution) {
|
|
157
220
|
for (const preResolution of opts.hooks.preResolution) {
|
|
158
221
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -227,15 +290,14 @@ export async function mutateModules(projects, maybeOpts) {
|
|
|
227
290
|
ignoredScriptsLogger.debug({
|
|
228
291
|
packageNames: ignoredBuilds ? dedupePackageNamesFromIgnoredBuilds(ignoredBuilds) : [],
|
|
229
292
|
});
|
|
230
|
-
|
|
231
|
-
streamParser.removeListener('data', reporter);
|
|
232
|
-
}
|
|
293
|
+
detachReporter();
|
|
233
294
|
return {
|
|
234
295
|
updatedCatalogs: result.updatedCatalogs,
|
|
235
296
|
updatedProjects: result.updatedProjects,
|
|
236
297
|
stats: result.stats ?? { added: 0, removed: 0, linkedToRoot: 0 },
|
|
237
298
|
depsRequiringBuild: result.depsRequiringBuild,
|
|
238
299
|
ignoredBuilds,
|
|
300
|
+
resolutionPolicyViolations: result.resolutionPolicyViolations ?? [],
|
|
239
301
|
};
|
|
240
302
|
async function _install() {
|
|
241
303
|
const scriptsOpts = {
|
|
@@ -523,6 +585,7 @@ export async function mutateModules(projects, maybeOpts) {
|
|
|
523
585
|
stats: result.stats,
|
|
524
586
|
depsRequiringBuild: result.depsRequiringBuild,
|
|
525
587
|
ignoredBuilds: result.ignoredBuilds,
|
|
588
|
+
resolutionPolicyViolations: result.resolutionPolicyViolations,
|
|
526
589
|
};
|
|
527
590
|
}
|
|
528
591
|
/**
|
|
@@ -633,6 +696,28 @@ Note that in CI environments, this setting is enabled by default.`,
|
|
|
633
696
|
else {
|
|
634
697
|
logger.info({ message: 'Lockfile is up to date, resolution step is skipped', prefix: opts.lockfileDir });
|
|
635
698
|
}
|
|
699
|
+
if (opts.runPacquet != null) {
|
|
700
|
+
try {
|
|
701
|
+
await opts.runPacquet();
|
|
702
|
+
}
|
|
703
|
+
catch (err) {
|
|
704
|
+
// Same reasoning as the verifyLockfileResolutions catch above: this
|
|
705
|
+
// is the user-facing failure path, so detach the reporter listener
|
|
706
|
+
// before rethrowing so long-lived processes don't leak it.
|
|
707
|
+
detachReporter();
|
|
708
|
+
throw err;
|
|
709
|
+
}
|
|
710
|
+
return {
|
|
711
|
+
updatedProjects: projects.map((mutatedProject) => {
|
|
712
|
+
const project = ctx.projects[mutatedProject.rootDir];
|
|
713
|
+
return {
|
|
714
|
+
...project,
|
|
715
|
+
manifest: project.originalManifest ?? project.manifest,
|
|
716
|
+
};
|
|
717
|
+
}),
|
|
718
|
+
ignoredBuilds: undefined,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
636
721
|
try {
|
|
637
722
|
const { stats, ignoredBuilds } = await headlessInstall({
|
|
638
723
|
...ctx,
|
|
@@ -811,7 +896,7 @@ function getPerDepCatalogName(wantedDep, globalSaveCatalogName) {
|
|
|
811
896
|
}
|
|
812
897
|
export async function addDependenciesToPackage(manifest, dependencySelectors, opts) {
|
|
813
898
|
const rootDir = (opts.dir ?? process.cwd());
|
|
814
|
-
const { updatedCatalogs, updatedProjects: projects, ignoredBuilds } = await mutateModules([
|
|
899
|
+
const { updatedCatalogs, updatedProjects: projects, ignoredBuilds, resolutionPolicyViolations } = await mutateModules([
|
|
815
900
|
{
|
|
816
901
|
allowNew: opts.allowNew,
|
|
817
902
|
dependencySelectors,
|
|
@@ -837,7 +922,7 @@ export async function addDependenciesToPackage(manifest, dependencySelectors, op
|
|
|
837
922
|
},
|
|
838
923
|
],
|
|
839
924
|
});
|
|
840
|
-
return { updatedCatalogs, updatedManifest: projects[0].manifest, ignoredBuilds };
|
|
925
|
+
return { updatedCatalogs, updatedManifest: projects[0].manifest, ignoredBuilds, resolutionPolicyViolations };
|
|
841
926
|
}
|
|
842
927
|
const _installInContext = async (projects, ctx, opts) => {
|
|
843
928
|
// The wanted lockfile is mutated during installation. To compare changes, a
|
|
@@ -910,7 +995,7 @@ const _installInContext = async (projects, ctx, opts) => {
|
|
|
910
995
|
// ignores preferred versions from the lockfile.
|
|
911
996
|
forgetResolutionsOfAllPrevWantedDeps(ctx.wantedLockfile);
|
|
912
997
|
}
|
|
913
|
-
let { dependenciesGraph, dependenciesByProjectId, linkedDependenciesByProjectId, updatedCatalogs, newLockfile, outdatedDependencies, peerDependencyIssuesByProjects, wantedToBeSkippedPackageIds, waitTillAllFetchingsFinish, } = await resolveDependencies(projects, {
|
|
998
|
+
let { dependenciesGraph, dependenciesByProjectId, linkedDependenciesByProjectId, updatedCatalogs, newLockfile, outdatedDependencies, peerDependencyIssuesByProjects, wantedToBeSkippedPackageIds, waitTillAllFetchingsFinish, resolutionPolicyViolations, } = await resolveDependencies(projects, {
|
|
914
999
|
allowBuild: opts.allowBuild,
|
|
915
1000
|
allowedDeprecatedVersions: opts.allowedDeprecatedVersions,
|
|
916
1001
|
allowUnusedPatches: opts.allowUnusedPatches,
|
|
@@ -964,6 +1049,7 @@ const _installInContext = async (projects, ctx, opts) => {
|
|
|
964
1049
|
trustPolicyIgnoreAfter: opts.trustPolicyIgnoreAfter,
|
|
965
1050
|
blockExoticSubdeps: opts.blockExoticSubdeps,
|
|
966
1051
|
allProjectIds: Object.values(ctx.projects).map((p) => p.id),
|
|
1052
|
+
handleResolutionPolicyViolations: opts.handleResolutionPolicyViolations,
|
|
967
1053
|
});
|
|
968
1054
|
if (!opts.include.optionalDependencies || !opts.include.devDependencies || !opts.include.dependencies) {
|
|
969
1055
|
linkedDependenciesByProjectId = mapValues((linkedDeps) => linkedDeps.filter((linkedDep) => !(linkedDep.dev && !opts.include.devDependencies ||
|
|
@@ -1178,11 +1264,13 @@ const _installInContext = async (projects, ctx, opts) => {
|
|
|
1178
1264
|
const currentLockfileDir = path.join(ctx.rootModulesDir, '.pnpm');
|
|
1179
1265
|
await Promise.all([
|
|
1180
1266
|
opts.useLockfile && opts.saveLockfile
|
|
1181
|
-
?
|
|
1267
|
+
? writeLockfilesAndRecordVerified({
|
|
1182
1268
|
currentLockfile: result.currentLockfile,
|
|
1183
1269
|
currentLockfileDir,
|
|
1184
1270
|
wantedLockfile: newLockfile,
|
|
1185
1271
|
wantedLockfileDir: ctx.lockfileDir,
|
|
1272
|
+
cacheDir: opts.cacheDir,
|
|
1273
|
+
resolutionVerifiers: opts.resolutionVerifiers,
|
|
1186
1274
|
...lockfileOpts,
|
|
1187
1275
|
})
|
|
1188
1276
|
: writeCurrentLockfile(ctx.virtualStoreDir, result.currentLockfile),
|
|
@@ -1234,10 +1322,24 @@ const _installInContext = async (projects, ctx, opts) => {
|
|
|
1234
1322
|
}
|
|
1235
1323
|
else {
|
|
1236
1324
|
if (opts.useLockfile && opts.saveLockfile && !isInstallationOnlyForLockfileCheck) {
|
|
1237
|
-
await
|
|
1325
|
+
await writeWantedLockfileAndRecordVerified({
|
|
1326
|
+
lockfileDir: ctx.lockfileDir,
|
|
1327
|
+
lockfile: newLockfile,
|
|
1328
|
+
cacheDir: opts.cacheDir,
|
|
1329
|
+
resolutionVerifiers: opts.resolutionVerifiers,
|
|
1330
|
+
...lockfileOpts,
|
|
1331
|
+
});
|
|
1238
1332
|
}
|
|
1239
|
-
if (opts.nodeLinker !== 'hoisted') {
|
|
1240
|
-
// This is only needed because otherwise the reporter will hang
|
|
1333
|
+
if (opts.nodeLinker !== 'hoisted' && opts.runPacquet == null) {
|
|
1334
|
+
// This is only needed because otherwise the reporter will hang.
|
|
1335
|
+
// Skipped when pacquet is about to take over the materialization
|
|
1336
|
+
// phase: the default reporter completes the progress stream for
|
|
1337
|
+
// this prefix on `importing_done`, so emitting it from the
|
|
1338
|
+
// lockfileOnly resolve pass would prematurely close the stream
|
|
1339
|
+
// and pacquet's own `importing_started` / progress events would
|
|
1340
|
+
// render to a stale stream. Pacquet emits its own
|
|
1341
|
+
// `importing_done` after the install, which closes the stream
|
|
1342
|
+
// normally.
|
|
1241
1343
|
stageLogger.debug({
|
|
1242
1344
|
prefix: opts.lockfileDir,
|
|
1243
1345
|
stage: 'importing_done',
|
|
@@ -1261,7 +1363,15 @@ const _installInContext = async (projects, ctx, opts) => {
|
|
|
1261
1363
|
strictPeerDependencies: opts.strictPeerDependencies,
|
|
1262
1364
|
rules: opts.peerDependencyRules,
|
|
1263
1365
|
});
|
|
1264
|
-
|
|
1366
|
+
// Skipped when pacquet will take over the materialization. The
|
|
1367
|
+
// default reporter's `reportSummary` `take(1)`s the first summary
|
|
1368
|
+
// event and combines it with whatever `pkgsDiff` it has at that
|
|
1369
|
+
// moment — which is empty here, since pacquet hasn't emitted its
|
|
1370
|
+
// per-direct-dep `pnpm:root` events yet. Letting pnpm fire summary
|
|
1371
|
+
// now would lock in an empty diff. Pacquet emits its own
|
|
1372
|
+
// `pnpm:summary` after the install completes, by which point its
|
|
1373
|
+
// root events have populated the diff.
|
|
1374
|
+
if (!opts.omitSummaryLog && opts.runPacquet == null) {
|
|
1265
1375
|
summaryLogger.debug({ prefix: opts.lockfileDir });
|
|
1266
1376
|
}
|
|
1267
1377
|
// Similar to the sequencing for when the original wanted lockfile is
|
|
@@ -1282,11 +1392,39 @@ const _installInContext = async (projects, ctx, opts) => {
|
|
|
1282
1392
|
stats,
|
|
1283
1393
|
depsRequiringBuild,
|
|
1284
1394
|
ignoredBuilds,
|
|
1395
|
+
resolutionPolicyViolations,
|
|
1285
1396
|
};
|
|
1286
1397
|
};
|
|
1287
1398
|
function allMutationsAreInstalls(projects) {
|
|
1288
1399
|
return projects.every((project) => project.mutation === 'install' && !project.update && !project.updateMatching);
|
|
1289
1400
|
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Run the pacquet binary if it's configured, otherwise run the JS
|
|
1403
|
+
* `headlessInstall`. Callers can hand off any code path that materializes
|
|
1404
|
+
* an already-resolved lockfile (workspace partial install, hoisted
|
|
1405
|
+
* linker, agent-server install, frozen install) without restating the
|
|
1406
|
+
* delegation choice.
|
|
1407
|
+
*
|
|
1408
|
+
* Pacquet reads the wanted lockfile from disk and produces its own
|
|
1409
|
+
* `pnpm:stats` / `pnpm:ignored-scripts` log events that drive the
|
|
1410
|
+
* reporter. The structured stats / ignoredBuilds return values that
|
|
1411
|
+
* `headlessInstall` produces aren't recovered here — pacquet doesn't
|
|
1412
|
+
* surface them through any return path — so callers get `undefined` for
|
|
1413
|
+
* both. `mutateModules` already tolerates that (it falls back to a zero
|
|
1414
|
+
* stats record and a no-op ignoredBuilds iteration).
|
|
1415
|
+
*/
|
|
1416
|
+
async function materializeOrDelegate(opts, runHeadlessInstall) {
|
|
1417
|
+
if (opts.runPacquet != null) {
|
|
1418
|
+
// Reached only from the resolve-then-materialize call sites
|
|
1419
|
+
// (workspace-partial, hoisted-linker, agent install). Each ran a
|
|
1420
|
+
// lockfileOnly resolve pass that emitted one
|
|
1421
|
+
// `pnpm:progress status:resolved` per package, so pacquet's
|
|
1422
|
+
// duplicate `resolved` events would double the reporter's count.
|
|
1423
|
+
await opts.runPacquet({ filterResolvedProgress: true });
|
|
1424
|
+
return {};
|
|
1425
|
+
}
|
|
1426
|
+
return runHeadlessInstall();
|
|
1427
|
+
}
|
|
1290
1428
|
const installInContext = async (projects, ctx, opts) => {
|
|
1291
1429
|
try {
|
|
1292
1430
|
const isPathInsideWorkspace = isSubdir.bind(null, opts.lockfileDir);
|
|
@@ -1323,7 +1461,7 @@ const installInContext = async (projects, ctx, opts) => {
|
|
|
1323
1461
|
...opts,
|
|
1324
1462
|
lockfileOnly: true,
|
|
1325
1463
|
});
|
|
1326
|
-
const { stats, ignoredBuilds } = await headlessInstall({
|
|
1464
|
+
const { stats, ignoredBuilds } = await materializeOrDelegate(opts, () => headlessInstall({
|
|
1327
1465
|
...ctx,
|
|
1328
1466
|
...opts,
|
|
1329
1467
|
currentEngine: {
|
|
@@ -1337,7 +1475,7 @@ const installInContext = async (projects, ctx, opts) => {
|
|
|
1337
1475
|
wantedLockfile: result.newLockfile,
|
|
1338
1476
|
useLockfile: opts.useLockfile && ctx.wantedLockfileIsModified,
|
|
1339
1477
|
hoistWorkspacePackages: opts.hoistWorkspacePackages,
|
|
1340
|
-
});
|
|
1478
|
+
}));
|
|
1341
1479
|
return {
|
|
1342
1480
|
...result,
|
|
1343
1481
|
stats,
|
|
@@ -1350,7 +1488,7 @@ const installInContext = async (projects, ctx, opts) => {
|
|
|
1350
1488
|
...opts,
|
|
1351
1489
|
lockfileOnly: true,
|
|
1352
1490
|
});
|
|
1353
|
-
const { stats, ignoredBuilds } = await headlessInstall({
|
|
1491
|
+
const { stats, ignoredBuilds } = await materializeOrDelegate(opts, () => headlessInstall({
|
|
1354
1492
|
...ctx,
|
|
1355
1493
|
...opts,
|
|
1356
1494
|
currentEngine: {
|
|
@@ -1364,13 +1502,29 @@ const installInContext = async (projects, ctx, opts) => {
|
|
|
1364
1502
|
wantedLockfile: result.newLockfile,
|
|
1365
1503
|
useLockfile: opts.useLockfile && ctx.wantedLockfileIsModified,
|
|
1366
1504
|
hoistWorkspacePackages: opts.hoistWorkspacePackages,
|
|
1367
|
-
});
|
|
1505
|
+
}));
|
|
1368
1506
|
return {
|
|
1369
1507
|
...result,
|
|
1370
1508
|
stats,
|
|
1371
1509
|
ignoredBuilds,
|
|
1372
1510
|
};
|
|
1373
1511
|
}
|
|
1512
|
+
// Isolated `nodeLinker` (the default) with a non-frozen install:
|
|
1513
|
+
// pacquet doesn't ship a resolver yet, so split the install in two —
|
|
1514
|
+
// ask `_installInContext` for a `lockfileOnly` resolve pass (writes
|
|
1515
|
+
// `pnpm-lock.yaml`), then hand the freshly-written lockfile to
|
|
1516
|
+
// pacquet for the fetch / import / link / build phases. The frozen
|
|
1517
|
+
// branch is handled earlier in `tryFrozenInstall`; the hoisted
|
|
1518
|
+
// branch above already runs the same resolve-then-materialize
|
|
1519
|
+
// sequence (it had to even before pacquet existed). When no pacquet
|
|
1520
|
+
// is configured this falls through to the full single-pass install.
|
|
1521
|
+
if (opts.runPacquet != null && !opts.lockfileOnly) {
|
|
1522
|
+
const result = await _installInContext(projects, ctx, { ...opts, lockfileOnly: true });
|
|
1523
|
+
// The resolve pass above emitted a `pnpm:progress status:resolved`
|
|
1524
|
+
// per package; ask pacquet to drop its own duplicates.
|
|
1525
|
+
await opts.runPacquet({ filterResolvedProgress: true });
|
|
1526
|
+
return result;
|
|
1527
|
+
}
|
|
1374
1528
|
return await _installInContext(projects, ctx, opts);
|
|
1375
1529
|
}
|
|
1376
1530
|
catch (error) { // eslint-disable-line
|
|
@@ -1645,6 +1799,14 @@ async function mutateModulesViaAgent(projects, opts) {
|
|
|
1645
1799
|
* packages into node_modules.
|
|
1646
1800
|
*/
|
|
1647
1801
|
async function installFromPnpmRegistry(manifest, rootDir, opts, allInstallProjects) {
|
|
1802
|
+
// The agent path skips client-side resolution, so resolver-side policies
|
|
1803
|
+
// can't be enforced locally. `minimumReleaseAge` is forwarded to the
|
|
1804
|
+
// agent and enforced server-side. `trustPolicy` has no server-side
|
|
1805
|
+
// counterpart yet, so refuse to run under it instead of silently
|
|
1806
|
+
// letting through a lockfile the local verifier would reject.
|
|
1807
|
+
if (opts.trustPolicy === 'no-downgrade') {
|
|
1808
|
+
throw new PnpmError('TRUST_POLICY_INCOMPATIBLE_WITH_AGENT', 'The pnpm agent does not yet enforce `trustPolicy: no-downgrade`, so running an install through the agent under this policy would produce a lockfile that the local verifier rejects.', { hint: 'Unset `trustPolicy` for this install, or disable the agent (unset `--agent` / `agent` in pnpm-workspace.yaml) so resolution runs locally and the trust check applies.' });
|
|
1809
|
+
}
|
|
1648
1810
|
const { fetchFromPnpmRegistry } = await import('@pnpm/agent.client');
|
|
1649
1811
|
const { StoreIndex } = await import('@pnpm/store.index');
|
|
1650
1812
|
const { setImportConcurrency } = await import('@pnpm/worker');
|
|
@@ -1695,7 +1857,14 @@ async function installFromPnpmRegistry(manifest, rootDir, opts, allInstallProjec
|
|
|
1695
1857
|
finally {
|
|
1696
1858
|
storeIndex.close();
|
|
1697
1859
|
}
|
|
1698
|
-
await
|
|
1860
|
+
await writeWantedLockfileAndRecordVerified({
|
|
1861
|
+
lockfileDir,
|
|
1862
|
+
lockfile,
|
|
1863
|
+
cacheDir: opts.cacheDir,
|
|
1864
|
+
resolutionVerifiers: opts.resolutionVerifiers,
|
|
1865
|
+
useGitBranchLockfile: opts.useGitBranchLockfile,
|
|
1866
|
+
mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
|
|
1867
|
+
});
|
|
1699
1868
|
logger.info({
|
|
1700
1869
|
message: `Resolved ${agentStats.totalPackages} packages: ${agentStats.alreadyInStore} cached, ${agentStats.filesToDownload} files to download`,
|
|
1701
1870
|
prefix: rootDir,
|
|
@@ -1767,14 +1936,25 @@ async function installFromPnpmRegistry(manifest, rootDir, opts, allInstallProjec
|
|
|
1767
1936
|
skipped: new Set(),
|
|
1768
1937
|
wantedLockfile: lockfile,
|
|
1769
1938
|
};
|
|
1939
|
+
const { ignoredBuilds, stats } = await materializeOrDelegate(opts,
|
|
1770
1940
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1771
|
-
|
|
1941
|
+
() => headlessInstall(headlessOpts));
|
|
1772
1942
|
return {
|
|
1773
1943
|
updatedCatalogs: undefined,
|
|
1774
1944
|
updatedManifest: manifest,
|
|
1775
1945
|
ignoredBuilds,
|
|
1776
|
-
stats
|
|
1946
|
+
// Pacquet doesn't surface a structured stats return; default to
|
|
1947
|
+
// zeros so the agent-path's non-optional `stats` slot is filled.
|
|
1948
|
+
// The reporter still renders accurate counts from pacquet's
|
|
1949
|
+
// `pnpm:stats` log events.
|
|
1950
|
+
stats: stats ?? { added: 0, removed: 0, linkedToRoot: 0 },
|
|
1777
1951
|
lockfile,
|
|
1952
|
+
// Server-side resolution (pnpm agent) enforces `minimumReleaseAge`
|
|
1953
|
+
// itself — the agent picks only mature versions and the lockfile
|
|
1954
|
+
// can't contain immature entries to auto-collect. `trustPolicy` is
|
|
1955
|
+
// guarded above (we refuse to enter this path when it's set), so
|
|
1956
|
+
// there's nothing for the install command to react to here.
|
|
1957
|
+
resolutionPolicyViolations: [],
|
|
1778
1958
|
};
|
|
1779
1959
|
}
|
|
1780
1960
|
finally {
|
package/lib/install/link.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { progressLogger, stageLogger, statsLogger, } from '@pnpm/core-loggers';
|
|
4
|
-
import { calcDepState } from '@pnpm/deps.graph-hasher';
|
|
4
|
+
import { calcDepState, findRuntimeNodeVersion } from '@pnpm/deps.graph-hasher';
|
|
5
5
|
import { symlinkDependency } from '@pnpm/fs.symlink-dependency';
|
|
6
6
|
import { linkDirectDeps } from '@pnpm/installing.linking.direct-dep-linker';
|
|
7
7
|
import { hoist } from '@pnpm/installing.linking.hoist';
|
|
@@ -318,6 +318,11 @@ async function selectNewFromWantedDeps(wantedRelDepPaths, currentLockfile, depGr
|
|
|
318
318
|
}
|
|
319
319
|
const limitLinking = pLimit(16);
|
|
320
320
|
async function linkAllPkgs(storeController, depNodes, opts) {
|
|
321
|
+
// Resolved `engines.runtime` Node version (when present) so the
|
|
322
|
+
// side-effects-cache key prefix tracks the script-runner Node
|
|
323
|
+
// rather than pnpm's own `process.version`. Computed once outside
|
|
324
|
+
// the per-node loop.
|
|
325
|
+
const nodeVersion = findRuntimeNodeVersion(Object.keys(opts.depGraph));
|
|
321
326
|
await Promise.all(depNodes.map(async (depNode) => {
|
|
322
327
|
const { files } = await depNode.fetching();
|
|
323
328
|
depNode.requiresBuild = files.requiresBuild;
|
|
@@ -328,6 +333,7 @@ async function linkAllPkgs(storeController, depNodes, opts) {
|
|
|
328
333
|
includeDepGraphHash: !opts.ignoreScripts && depNode.requiresBuild, // true when is built
|
|
329
334
|
patchFileHash: depNode.patch?.hash,
|
|
330
335
|
supportedArchitectures: opts.supportedArchitectures,
|
|
336
|
+
nodeVersion,
|
|
331
337
|
});
|
|
332
338
|
}
|
|
333
339
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { LockfileObject } from '@pnpm/lockfile.fs';
|
|
2
|
+
import type { ResolutionVerifier } from '@pnpm/resolving.resolver-base';
|
|
3
|
+
export interface RecordLockfileVerifiedOptions {
|
|
4
|
+
cacheDir?: string;
|
|
5
|
+
/** Absolute path of the lockfile the next install will read.
|
|
6
|
+
* Under `useGitBranchLockfile` this is the branch-suffixed name. */
|
|
7
|
+
lockfilePath: string;
|
|
8
|
+
/** The writer's canonical return value — see {@link writeWantedLockfile}.
|
|
9
|
+
* Passing the raw in-memory write object would record a hash the
|
|
10
|
+
* next install can't match (YAML drops undefined fields). */
|
|
11
|
+
lockfile: LockfileObject;
|
|
12
|
+
resolutionVerifiers: readonly ResolutionVerifier[] | undefined;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Records the post-resolution lockfile as verified so the next install
|
|
16
|
+
* skips the registry round-trip. Skipping is safe: fresh local picks
|
|
17
|
+
* are filtered by the resolver (see
|
|
18
|
+
* `resolving/npm-resolver/src/pickPackage.ts`) and carried-over entries
|
|
19
|
+
* already passed the gate at the top of `mutateModules`, so the
|
|
20
|
+
* recorded lockfile is policy-clean by construction.
|
|
21
|
+
*/
|
|
22
|
+
export declare function recordLockfileVerified(opts: RecordLockfileVerifiedOptions): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { hashObject } from '@pnpm/crypto.object-hasher';
|
|
2
|
+
import { recordVerification } from './verifyLockfileResolutionsCache.js';
|
|
3
|
+
/**
|
|
4
|
+
* Records the post-resolution lockfile as verified so the next install
|
|
5
|
+
* skips the registry round-trip. Skipping is safe: fresh local picks
|
|
6
|
+
* are filtered by the resolver (see
|
|
7
|
+
* `resolving/npm-resolver/src/pickPackage.ts`) and carried-over entries
|
|
8
|
+
* already passed the gate at the top of `mutateModules`, so the
|
|
9
|
+
* recorded lockfile is policy-clean by construction.
|
|
10
|
+
*/
|
|
11
|
+
export function recordLockfileVerified(opts) {
|
|
12
|
+
if (!opts.cacheDir)
|
|
13
|
+
return;
|
|
14
|
+
if (!opts.resolutionVerifiers?.length)
|
|
15
|
+
return;
|
|
16
|
+
if (!opts.lockfile.packages)
|
|
17
|
+
return;
|
|
18
|
+
recordVerification(opts.cacheDir, {
|
|
19
|
+
lockfilePath: opts.lockfilePath,
|
|
20
|
+
verifiers: opts.resolutionVerifiers,
|
|
21
|
+
hashLockfile: () => hashObject(opts.lockfile),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=recordLockfileVerified.js.map
|