@pnpm/installing.deps-installer 1101.1.2 → 1101.2.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 +34 -1
- package/lib/install/extendInstallOptions.js +1 -0
- package/lib/install/index.d.ts +14 -1
- package/lib/install/index.js +82 -12
- 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 +46 -46
|
@@ -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,6 +148,39 @@ 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;
|
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,36 @@ 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
|
+
const cacheActive = opts.cacheDir != null && opts.resolutionVerifiers.length > 0;
|
|
175
|
+
const wantedLockfilePath = cacheActive
|
|
176
|
+
? path.resolve(ctx.lockfileDir, await getWantedLockfileName({
|
|
177
|
+
useGitBranchLockfile: opts.useGitBranchLockfile,
|
|
178
|
+
mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
|
|
179
|
+
}))
|
|
180
|
+
: undefined;
|
|
181
|
+
try {
|
|
182
|
+
await verifyLockfileResolutions(ctx.wantedLockfile, opts.resolutionVerifiers, {
|
|
183
|
+
cacheDir: opts.cacheDir,
|
|
184
|
+
lockfilePath: wantedLockfilePath,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
// verifyLockfileResolutions is the one throw site in this function
|
|
189
|
+
// that's part of normal user-facing operation (a rejected lockfile);
|
|
190
|
+
// other throws here are unexpected. Detach the reporter listener so
|
|
191
|
+
// long-lived processes don't leak it on every rejected install.
|
|
192
|
+
detachReporter();
|
|
193
|
+
throw err;
|
|
194
|
+
}
|
|
156
195
|
if (opts.hooks.preResolution) {
|
|
157
196
|
for (const preResolution of opts.hooks.preResolution) {
|
|
158
197
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -227,15 +266,14 @@ export async function mutateModules(projects, maybeOpts) {
|
|
|
227
266
|
ignoredScriptsLogger.debug({
|
|
228
267
|
packageNames: ignoredBuilds ? dedupePackageNamesFromIgnoredBuilds(ignoredBuilds) : [],
|
|
229
268
|
});
|
|
230
|
-
|
|
231
|
-
streamParser.removeListener('data', reporter);
|
|
232
|
-
}
|
|
269
|
+
detachReporter();
|
|
233
270
|
return {
|
|
234
271
|
updatedCatalogs: result.updatedCatalogs,
|
|
235
272
|
updatedProjects: result.updatedProjects,
|
|
236
273
|
stats: result.stats ?? { added: 0, removed: 0, linkedToRoot: 0 },
|
|
237
274
|
depsRequiringBuild: result.depsRequiringBuild,
|
|
238
275
|
ignoredBuilds,
|
|
276
|
+
resolutionPolicyViolations: result.resolutionPolicyViolations ?? [],
|
|
239
277
|
};
|
|
240
278
|
async function _install() {
|
|
241
279
|
const scriptsOpts = {
|
|
@@ -523,6 +561,7 @@ export async function mutateModules(projects, maybeOpts) {
|
|
|
523
561
|
stats: result.stats,
|
|
524
562
|
depsRequiringBuild: result.depsRequiringBuild,
|
|
525
563
|
ignoredBuilds: result.ignoredBuilds,
|
|
564
|
+
resolutionPolicyViolations: result.resolutionPolicyViolations,
|
|
526
565
|
};
|
|
527
566
|
}
|
|
528
567
|
/**
|
|
@@ -811,7 +850,7 @@ function getPerDepCatalogName(wantedDep, globalSaveCatalogName) {
|
|
|
811
850
|
}
|
|
812
851
|
export async function addDependenciesToPackage(manifest, dependencySelectors, opts) {
|
|
813
852
|
const rootDir = (opts.dir ?? process.cwd());
|
|
814
|
-
const { updatedCatalogs, updatedProjects: projects, ignoredBuilds } = await mutateModules([
|
|
853
|
+
const { updatedCatalogs, updatedProjects: projects, ignoredBuilds, resolutionPolicyViolations } = await mutateModules([
|
|
815
854
|
{
|
|
816
855
|
allowNew: opts.allowNew,
|
|
817
856
|
dependencySelectors,
|
|
@@ -837,7 +876,7 @@ export async function addDependenciesToPackage(manifest, dependencySelectors, op
|
|
|
837
876
|
},
|
|
838
877
|
],
|
|
839
878
|
});
|
|
840
|
-
return { updatedCatalogs, updatedManifest: projects[0].manifest, ignoredBuilds };
|
|
879
|
+
return { updatedCatalogs, updatedManifest: projects[0].manifest, ignoredBuilds, resolutionPolicyViolations };
|
|
841
880
|
}
|
|
842
881
|
const _installInContext = async (projects, ctx, opts) => {
|
|
843
882
|
// The wanted lockfile is mutated during installation. To compare changes, a
|
|
@@ -910,7 +949,7 @@ const _installInContext = async (projects, ctx, opts) => {
|
|
|
910
949
|
// ignores preferred versions from the lockfile.
|
|
911
950
|
forgetResolutionsOfAllPrevWantedDeps(ctx.wantedLockfile);
|
|
912
951
|
}
|
|
913
|
-
let { dependenciesGraph, dependenciesByProjectId, linkedDependenciesByProjectId, updatedCatalogs, newLockfile, outdatedDependencies, peerDependencyIssuesByProjects, wantedToBeSkippedPackageIds, waitTillAllFetchingsFinish, } = await resolveDependencies(projects, {
|
|
952
|
+
let { dependenciesGraph, dependenciesByProjectId, linkedDependenciesByProjectId, updatedCatalogs, newLockfile, outdatedDependencies, peerDependencyIssuesByProjects, wantedToBeSkippedPackageIds, waitTillAllFetchingsFinish, resolutionPolicyViolations, } = await resolveDependencies(projects, {
|
|
914
953
|
allowBuild: opts.allowBuild,
|
|
915
954
|
allowedDeprecatedVersions: opts.allowedDeprecatedVersions,
|
|
916
955
|
allowUnusedPatches: opts.allowUnusedPatches,
|
|
@@ -964,6 +1003,7 @@ const _installInContext = async (projects, ctx, opts) => {
|
|
|
964
1003
|
trustPolicyIgnoreAfter: opts.trustPolicyIgnoreAfter,
|
|
965
1004
|
blockExoticSubdeps: opts.blockExoticSubdeps,
|
|
966
1005
|
allProjectIds: Object.values(ctx.projects).map((p) => p.id),
|
|
1006
|
+
handleResolutionPolicyViolations: opts.handleResolutionPolicyViolations,
|
|
967
1007
|
});
|
|
968
1008
|
if (!opts.include.optionalDependencies || !opts.include.devDependencies || !opts.include.dependencies) {
|
|
969
1009
|
linkedDependenciesByProjectId = mapValues((linkedDeps) => linkedDeps.filter((linkedDep) => !(linkedDep.dev && !opts.include.devDependencies ||
|
|
@@ -1178,11 +1218,13 @@ const _installInContext = async (projects, ctx, opts) => {
|
|
|
1178
1218
|
const currentLockfileDir = path.join(ctx.rootModulesDir, '.pnpm');
|
|
1179
1219
|
await Promise.all([
|
|
1180
1220
|
opts.useLockfile && opts.saveLockfile
|
|
1181
|
-
?
|
|
1221
|
+
? writeLockfilesAndRecordVerified({
|
|
1182
1222
|
currentLockfile: result.currentLockfile,
|
|
1183
1223
|
currentLockfileDir,
|
|
1184
1224
|
wantedLockfile: newLockfile,
|
|
1185
1225
|
wantedLockfileDir: ctx.lockfileDir,
|
|
1226
|
+
cacheDir: opts.cacheDir,
|
|
1227
|
+
resolutionVerifiers: opts.resolutionVerifiers,
|
|
1186
1228
|
...lockfileOpts,
|
|
1187
1229
|
})
|
|
1188
1230
|
: writeCurrentLockfile(ctx.virtualStoreDir, result.currentLockfile),
|
|
@@ -1234,7 +1276,13 @@ const _installInContext = async (projects, ctx, opts) => {
|
|
|
1234
1276
|
}
|
|
1235
1277
|
else {
|
|
1236
1278
|
if (opts.useLockfile && opts.saveLockfile && !isInstallationOnlyForLockfileCheck) {
|
|
1237
|
-
await
|
|
1279
|
+
await writeWantedLockfileAndRecordVerified({
|
|
1280
|
+
lockfileDir: ctx.lockfileDir,
|
|
1281
|
+
lockfile: newLockfile,
|
|
1282
|
+
cacheDir: opts.cacheDir,
|
|
1283
|
+
resolutionVerifiers: opts.resolutionVerifiers,
|
|
1284
|
+
...lockfileOpts,
|
|
1285
|
+
});
|
|
1238
1286
|
}
|
|
1239
1287
|
if (opts.nodeLinker !== 'hoisted') {
|
|
1240
1288
|
// This is only needed because otherwise the reporter will hang
|
|
@@ -1282,6 +1330,7 @@ const _installInContext = async (projects, ctx, opts) => {
|
|
|
1282
1330
|
stats,
|
|
1283
1331
|
depsRequiringBuild,
|
|
1284
1332
|
ignoredBuilds,
|
|
1333
|
+
resolutionPolicyViolations,
|
|
1285
1334
|
};
|
|
1286
1335
|
};
|
|
1287
1336
|
function allMutationsAreInstalls(projects) {
|
|
@@ -1645,6 +1694,14 @@ async function mutateModulesViaAgent(projects, opts) {
|
|
|
1645
1694
|
* packages into node_modules.
|
|
1646
1695
|
*/
|
|
1647
1696
|
async function installFromPnpmRegistry(manifest, rootDir, opts, allInstallProjects) {
|
|
1697
|
+
// The agent path skips client-side resolution, so resolver-side policies
|
|
1698
|
+
// can't be enforced locally. `minimumReleaseAge` is forwarded to the
|
|
1699
|
+
// agent and enforced server-side. `trustPolicy` has no server-side
|
|
1700
|
+
// counterpart yet, so refuse to run under it instead of silently
|
|
1701
|
+
// letting through a lockfile the local verifier would reject.
|
|
1702
|
+
if (opts.trustPolicy === 'no-downgrade') {
|
|
1703
|
+
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.' });
|
|
1704
|
+
}
|
|
1648
1705
|
const { fetchFromPnpmRegistry } = await import('@pnpm/agent.client');
|
|
1649
1706
|
const { StoreIndex } = await import('@pnpm/store.index');
|
|
1650
1707
|
const { setImportConcurrency } = await import('@pnpm/worker');
|
|
@@ -1695,7 +1752,14 @@ async function installFromPnpmRegistry(manifest, rootDir, opts, allInstallProjec
|
|
|
1695
1752
|
finally {
|
|
1696
1753
|
storeIndex.close();
|
|
1697
1754
|
}
|
|
1698
|
-
await
|
|
1755
|
+
await writeWantedLockfileAndRecordVerified({
|
|
1756
|
+
lockfileDir,
|
|
1757
|
+
lockfile,
|
|
1758
|
+
cacheDir: opts.cacheDir,
|
|
1759
|
+
resolutionVerifiers: opts.resolutionVerifiers,
|
|
1760
|
+
useGitBranchLockfile: opts.useGitBranchLockfile,
|
|
1761
|
+
mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
|
|
1762
|
+
});
|
|
1699
1763
|
logger.info({
|
|
1700
1764
|
message: `Resolved ${agentStats.totalPackages} packages: ${agentStats.alreadyInStore} cached, ${agentStats.filesToDownload} files to download`,
|
|
1701
1765
|
prefix: rootDir,
|
|
@@ -1775,6 +1839,12 @@ async function installFromPnpmRegistry(manifest, rootDir, opts, allInstallProjec
|
|
|
1775
1839
|
ignoredBuilds,
|
|
1776
1840
|
stats,
|
|
1777
1841
|
lockfile,
|
|
1842
|
+
// Server-side resolution (pnpm agent) enforces `minimumReleaseAge`
|
|
1843
|
+
// itself — the agent picks only mature versions and the lockfile
|
|
1844
|
+
// can't contain immature entries to auto-collect. `trustPolicy` is
|
|
1845
|
+
// guarded above (we refuse to enter this path when it's set), so
|
|
1846
|
+
// there's nothing for the install command to react to here.
|
|
1847
|
+
resolutionPolicyViolations: [],
|
|
1778
1848
|
};
|
|
1779
1849
|
}
|
|
1780
1850
|
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
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { LockfileObject } from '@pnpm/lockfile.fs';
|
|
2
|
+
import type { ResolutionPolicyViolation, ResolutionVerifier } from '@pnpm/resolving.resolver-base';
|
|
3
|
+
export type { ResolutionPolicyViolation };
|
|
4
|
+
export interface VerifyLockfileResolutionsOptions {
|
|
5
|
+
concurrency?: number;
|
|
6
|
+
/**
|
|
7
|
+
* pnpm's on-disk cache directory. When set together with
|
|
8
|
+
* `lockfilePath`, verification results are memoized in
|
|
9
|
+
* `<cacheDir>/lockfile-verified.jsonl` and the gate short-circuits on
|
|
10
|
+
* a repeat run against an unchanged lockfile + same-or-stricter
|
|
11
|
+
* policy. Omit to disable the cache entirely (every call rehashes
|
|
12
|
+
* the lockfile and re-verifies).
|
|
13
|
+
*/
|
|
14
|
+
cacheDir?: string;
|
|
15
|
+
/** Absolute path of the lockfile being verified. Used by the cache's stat shortcut. */
|
|
16
|
+
lockfilePath?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Policy-neutral pass that asks every resolver-supplied
|
|
20
|
+
* {@link ResolutionVerifier} to check every entry in a lockfile loaded
|
|
21
|
+
* from disk. Iteration runs before resolution decisions are touched and
|
|
22
|
+
* before any tarball is fetched, so a lockfile whose entries were
|
|
23
|
+
* resolved elsewhere (committed to the repo, restored from a cache,
|
|
24
|
+
* etc.) under a weaker or absent policy cannot reach the filesystem.
|
|
25
|
+
* Fresh local resolution is covered by the resolver's own per-version
|
|
26
|
+
* filter.
|
|
27
|
+
*
|
|
28
|
+
* Each verifier handles its own protocol short-circuit inside `verify`
|
|
29
|
+
* (returning `{ ok: true }` for resolutions outside its scope), so the
|
|
30
|
+
* fan-out is policy-neutral and dispatch-free at this layer.
|
|
31
|
+
*
|
|
32
|
+
* Designed for fail-closed semantics at the verifier level: a verifier
|
|
33
|
+
* that can't confirm a resolution is expected to return `{ ok: false }`
|
|
34
|
+
* rather than passing silently — otherwise a registry hiccup or an
|
|
35
|
+
* unpublished version would re-open the bypass.
|
|
36
|
+
*
|
|
37
|
+
* No-op when `verifiers` is empty.
|
|
38
|
+
*
|
|
39
|
+
* When `options.cacheDir` and `options.lockfilePath` are both
|
|
40
|
+
* provided, an unchanged lockfile that has already been verified
|
|
41
|
+
* under the same (or stricter) policy short-circuits the registry
|
|
42
|
+
* round-trip entirely — see {@link tryLockfileVerificationCache} for
|
|
43
|
+
* the lookup logic.
|
|
44
|
+
*/
|
|
45
|
+
export declare function verifyLockfileResolutions(lockfile: LockfileObject, verifiers: ResolutionVerifier[], options?: VerifyLockfileResolutionsOptions): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Collect-mode sibling of {@link verifyLockfileResolutions}: runs the
|
|
48
|
+
* same fan-out over every verifier and every lockfile entry, but
|
|
49
|
+
* returns the violations as data instead of throwing on the first batch.
|
|
50
|
+
* No cache lookup or write — the throw-mode `verifyLockfileResolutions`
|
|
51
|
+
* is what populates / honors the cache; this is for callers that need
|
|
52
|
+
* to inspect violations (auto-collect into `minimumReleaseAgeExclude`,
|
|
53
|
+
* the strict-mode interactive prompt, future resolver-specific
|
|
54
|
+
* policies).
|
|
55
|
+
*
|
|
56
|
+
* Returns an empty array when `verifiers` is empty or the lockfile has
|
|
57
|
+
* no packages, so callers don't need a separate emptiness check.
|
|
58
|
+
*/
|
|
59
|
+
export declare function collectResolutionPolicyViolations(lockfile: LockfileObject, verifiers: ResolutionVerifier[], options?: Pick<VerifyLockfileResolutionsOptions, 'concurrency'>): Promise<ResolutionPolicyViolation[]>;
|