@pnpm/deps.graph-hasher 1100.2.18 → 1100.3.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @pnpm/calc-dep-state
2
2
 
3
+ ## 1100.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Fixed global virtual store hashes for dependency cycles. Every package that transitively depends on an allowed build now includes the engine in its store path, independent of traversal order [pnpm/pnpm#14341](https://github.com/pnpm/pnpm/issues/14341).
8
+
9
+ - Updated dependencies:
10
+ - @pnpm/lockfile.types@1100.1.1
11
+ - @pnpm/lockfile.utils@1102.1.1
12
+
13
+ ## 1100.3.0
14
+
15
+ ### Minor Changes
16
+
17
+ - Added an opt-in proof of concept that lets installs reuse a dependency's build output across machines, by publishing and restoring signed, organization-scoped artifacts through pnpr instead of running the lifecycle scripts locally.
18
+
19
+ Configure it with the new `remoteSideEffectsCache` setting. A workspace names the eligible `organization` and `packages`; everything describing the act of signing — `publish`, `keyId`, `builderId`, `trustedKeys`, `privateKey` and the provenance fields — is refused in `pnpm-workspace.yaml` and read from the global config file or the environment instead.
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies:
24
+ - @pnpm/deps.path@1101.0.1
25
+ - @pnpm/engine.runtime.system-version@1100.0.11
26
+ - @pnpm/lockfile.types@1100.1.0
27
+ - @pnpm/lockfile.utils@1102.1.0
28
+ - @pnpm/resolving.resolver-base@1101.2.0
29
+ - @pnpm/types@1102.1.0
30
+
3
31
  ## 1100.2.18
4
32
 
5
33
  ### Patch Changes
package/lib/index.d.ts CHANGED
@@ -51,6 +51,31 @@ export interface DepsGraphNode<T extends string> {
51
51
  export interface DepsStateCache {
52
52
  [depPath: string]: string;
53
53
  }
54
+ export declare const DEPENDENCY_SIDE_EFFECTS_INPUT_KEY_PREFIX = "dependency-side-effects:v1:";
55
+ export interface CalcDepStateInputKeyOptions<T extends string> {
56
+ depsGraph: DepsGraph<T>;
57
+ depPath: T;
58
+ patchFileHash?: string;
59
+ supportedArchitectures?: SupportedArchitectures;
60
+ }
61
+ /**
62
+ * Compute the machine-independent lookup key for a remotely shareable
63
+ * dependency build.
64
+ *
65
+ * `depsGraph` must contain `depPath`, and every reachable node must provide
66
+ * either `fullPkgId` or the resolution metadata needed to derive it. The
67
+ * function does not mutate the graph or caller state. Each call uses an
68
+ * isolated cache, so the result is independent of earlier roots and platform
69
+ * selections.
70
+ *
71
+ * The returned key starts with {@link DEPENDENCY_SIDE_EFFECTS_INPUT_KEY_PREFIX},
72
+ * followed by the recursive dependency-graph hash and, when non-empty, the
73
+ * patch-file hash. Host identity is excluded and advertised by the artifact's
74
+ * signed compatibility constraints. When a resolution contains platform
75
+ * variations, `supportedArchitectures` selects the source integrity included
76
+ * in the graph hash.
77
+ */
78
+ export declare function calcDepStateInputKey<T extends string>(opts: CalcDepStateInputKeyOptions<T>): string;
54
79
  export declare function calcDepState<T extends string>(depsGraph: DepsGraph<T>, cache: DepsStateCache, depPath: string, opts: {
55
80
  patchFileHash?: string;
56
81
  includeDepGraphHash: boolean;
@@ -101,11 +126,11 @@ export interface GraphNodeHashOptions {
101
126
  lockfileDir?: string;
102
127
  }
103
128
  export declare function iterateHashedGraphNodes<T extends PkgMeta>(graph: DepsGraph<DepPath>, pkgMetaIterator: PkgMetaIterator<T>, opts?: GraphNodeHashOptions): IterableIterator<HashedDepPath<T>>;
104
- export declare function calcGraphNodeHash<T extends PkgMeta>({ graph, cache, builtDepPaths, buildRequiredCache, supportedArchitectures, nodeVersion, lockfileDir }: {
129
+ export declare function calcGraphNodeHash<T extends PkgMeta>({ graph, cache, buildRequiredDepPaths, supportedArchitectures, nodeVersion, lockfileDir }: {
105
130
  graph: DepsGraph<DepPath>;
106
131
  cache: DepsStateCache;
107
- builtDepPaths?: Set<DepPath>;
108
- buildRequiredCache?: Record<string, boolean>;
132
+ /** See {@link computeBuildRequiredDepPaths}. */
133
+ buildRequiredDepPaths?: Set<DepPath>;
109
134
  supportedArchitectures?: SupportedArchitectures;
110
135
  /** See {@link GraphNodeHashOptions.nodeVersion}. */
111
136
  nodeVersion?: string;
@@ -127,3 +152,18 @@ export interface PkgMetaAndSnapshot extends PkgMeta {
127
152
  }
128
153
  export declare function iteratePkgMeta(lockfile: LockfileObject, graph: DepsGraph<DepPath>): PkgMetaIterator<PkgMetaAndSnapshot>;
129
154
  export declare function lockfileToDepGraph(lockfile: LockfileObject, supportedArchitectures?: SupportedArchitectures, lockfileDir?: string): DepsGraph<DepPath>;
155
+ /**
156
+ * Expand `builtDepPaths` to every node that is, or transitively depends
157
+ * on, one of them.
158
+ *
159
+ * The result gates the engine string in {@link calcGraphNodeHash}: only a
160
+ * package that may run a build script — or that depends on one — keeps the
161
+ * engine in its GVS hash. It is computed as one graph-wide reverse closure,
162
+ * so a package inside a dependency cycle gets the same answer no matter
163
+ * which node the hasher reaches it from.
164
+ *
165
+ * A path with no node in `graph` is kept and still marks whatever depends on
166
+ * it: the built set comes from the allowBuild policy rather than the graph,
167
+ * so the two can disagree.
168
+ */
169
+ export declare function computeBuildRequiredDepPaths(graph: DepsGraph<DepPath>, builtDepPaths: Set<DepPath>): Set<DepPath>;
package/lib/index.js CHANGED
@@ -71,11 +71,52 @@ export function readSnapshotRuntimePin(children) {
71
71
  const ref = children?.node;
72
72
  return ref != null ? extractRuntimeNodeVersion(ref) : undefined;
73
73
  }
74
+ export const DEPENDENCY_SIDE_EFFECTS_INPUT_KEY_PREFIX = 'dependency-side-effects:v1:';
75
+ /**
76
+ * Compute the machine-independent lookup key for a remotely shareable
77
+ * dependency build.
78
+ *
79
+ * `depsGraph` must contain `depPath`, and every reachable node must provide
80
+ * either `fullPkgId` or the resolution metadata needed to derive it. The
81
+ * function does not mutate the graph or caller state. Each call uses an
82
+ * isolated cache, so the result is independent of earlier roots and platform
83
+ * selections.
84
+ *
85
+ * The returned key starts with {@link DEPENDENCY_SIDE_EFFECTS_INPUT_KEY_PREFIX},
86
+ * followed by the recursive dependency-graph hash and, when non-empty, the
87
+ * patch-file hash. Host identity is excluded and advertised by the artifact's
88
+ * signed compatibility constraints. When a resolution contains platform
89
+ * variations, `supportedArchitectures` selects the source integrity included
90
+ * in the graph hash.
91
+ */
92
+ export function calcDepStateInputKey(opts) {
93
+ if (opts.depsGraph[opts.depPath] == null) {
94
+ throw new Error(`Dependency side-effects input-key root ${opts.depPath} is not present in depsGraph`);
95
+ }
96
+ const depGraphHash = calcDepGraphHash({
97
+ depsGraph: opts.depsGraph,
98
+ cache: {},
99
+ parents: new Set(),
100
+ depPath: opts.depPath,
101
+ context: createDepGraphHashContext(opts.supportedArchitectures),
102
+ });
103
+ let result = `${DEPENDENCY_SIDE_EFFECTS_INPUT_KEY_PREFIX}deps=${depGraphHash}`;
104
+ if (opts.patchFileHash) {
105
+ result += `;patch=${opts.patchFileHash}`;
106
+ }
107
+ return result;
108
+ }
74
109
  export function calcDepState(depsGraph, cache, depPath, opts) {
75
110
  const ownPin = readSnapshotRuntimePin(depsGraph[depPath]?.children);
76
111
  let result = engineName(ownPin ?? opts.nodeVersion);
77
112
  if (opts.includeDepGraphHash) {
78
- const depGraphHash = calcDepGraphHash(depsGraph, cache, new Set(), depPath, opts.supportedArchitectures);
113
+ const depGraphHash = calcDepGraphHash({
114
+ depsGraph,
115
+ cache,
116
+ parents: new Set(),
117
+ depPath: depPath,
118
+ context: createDepGraphHashContext(opts.supportedArchitectures),
119
+ });
79
120
  result += `;deps=${depGraphHash}`;
80
121
  }
81
122
  if (opts.patchFileHash) {
@@ -83,43 +124,54 @@ export function calcDepState(depsGraph, cache, depPath, opts) {
83
124
  }
84
125
  return result;
85
126
  }
86
- function calcDepGraphHash(depsGraph, cache, parents, depPath, supportedArchitectures) {
87
- if (cache[depPath])
88
- return cache[depPath];
127
+ function calcDepGraphHash({ depsGraph, cache, parents, depPath, context, }) {
128
+ const cacheKey = `${context.cacheKeyPrefix}${depPath}`;
129
+ if (cache[cacheKey])
130
+ return cache[cacheKey];
89
131
  const node = depsGraph[depPath];
90
132
  if (!node)
91
133
  return '';
92
- if (!node.fullPkgId) {
93
- if (!node.pkgIdWithPatchHash) {
94
- throw new Error(`pkgIdWithPatchHash is not defined for ${depPath} in depsGraph`);
95
- }
96
- if (!node.resolution) {
97
- throw new Error(`resolution is not defined for ${depPath} in depsGraph`);
98
- }
99
- node.fullPkgId = createFullPkgId(node.pkgIdWithPatchHash, node.resolution, supportedArchitectures);
134
+ let fullPkgId = node.fullPkgId;
135
+ if (node.pkgIdWithPatchHash != null && node.resolution != null) {
136
+ fullPkgId = createFullPkgId(node.pkgIdWithPatchHash, node.resolution, context.supportedArchitectures);
137
+ }
138
+ else if (fullPkgId == null) {
139
+ throw new Error(`fullPkgId or resolution metadata is not defined for ${depPath} in depsGraph`);
100
140
  }
101
141
  const deps = {};
102
- if (Object.keys(node.children).length && !parents.has(node.fullPkgId)) {
103
- const nextParents = new Set([...Array.from(parents), node.fullPkgId]);
142
+ if (Object.keys(node.children).length && !parents.has(fullPkgId)) {
143
+ const nextParents = new Set([...Array.from(parents), fullPkgId]);
104
144
  for (const alias in node.children) {
105
145
  if (Object.hasOwn(node.children, alias)) {
106
146
  const childId = node.children[alias];
107
- deps[alias] = calcDepGraphHash(depsGraph, cache, nextParents, childId, supportedArchitectures);
147
+ deps[alias] = calcDepGraphHash({
148
+ depsGraph,
149
+ cache,
150
+ parents: nextParents,
151
+ depPath: childId,
152
+ context,
153
+ });
108
154
  }
109
155
  }
110
156
  }
111
- cache[depPath] = hashObject({
112
- id: node.fullPkgId,
157
+ cache[cacheKey] = hashObject({
158
+ id: fullPkgId,
113
159
  deps,
114
160
  });
115
- return cache[depPath];
161
+ return cache[cacheKey];
162
+ }
163
+ function createDepGraphHashContext(supportedArchitectures) {
164
+ return {
165
+ supportedArchitectures,
166
+ cacheKeyPrefix: supportedArchitectures == null ? '' : `\0architectures=${hashObject(supportedArchitectures)}\0`,
167
+ };
116
168
  }
117
169
  export function* iterateHashedGraphNodes(graph, pkgMetaIterator, opts = {}) {
118
- let builtDepPaths;
170
+ let buildRequiredDepPaths;
119
171
  let entries;
120
172
  if (opts.allowBuild != null) {
121
173
  const pkgMetaList = Array.from(pkgMetaIterator);
122
- builtDepPaths = computeBuiltDepPaths(pkgMetaList, opts.allowBuild);
174
+ buildRequiredDepPaths = computeBuildRequiredDepPaths(graph, computeBuiltDepPaths(pkgMetaList, opts.allowBuild));
123
175
  entries = pkgMetaList;
124
176
  }
125
177
  else {
@@ -128,8 +180,7 @@ export function* iterateHashedGraphNodes(graph, pkgMetaIterator, opts = {}) {
128
180
  const ctx = {
129
181
  graph,
130
182
  cache: {},
131
- builtDepPaths,
132
- buildRequiredCache: builtDepPaths !== undefined ? {} : undefined,
183
+ buildRequiredDepPaths,
133
184
  supportedArchitectures: opts.supportedArchitectures,
134
185
  nodeVersion: opts.nodeVersion,
135
186
  lockfileDir: opts.lockfileDir,
@@ -141,15 +192,14 @@ export function* iterateHashedGraphNodes(graph, pkgMetaIterator, opts = {}) {
141
192
  };
142
193
  }
143
194
  }
144
- export function calcGraphNodeHash({ graph, cache, builtDepPaths, buildRequiredCache, supportedArchitectures, nodeVersion, lockfileDir }, pkgMeta) {
195
+ export function calcGraphNodeHash({ graph, cache, buildRequiredDepPaths, supportedArchitectures, nodeVersion, lockfileDir }, pkgMeta) {
145
196
  const { name, version, depPath } = pkgMeta;
146
- // When builtDepPaths is provided (derived from the allowBuilds config),
147
- // we only include the engine name for packages that are allowed to build
148
- // or transitively depend on a package that is allowed to build.
197
+ // When buildRequiredDepPaths is provided (derived from the allowBuilds
198
+ // config), we only include the engine name for packages that are allowed
199
+ // to build or transitively depend on a package that is allowed to build.
149
200
  // This makes GVS hashes engine-agnostic for pure-JS packages,
150
201
  // so they survive Node.js upgrades and architecture changes.
151
- const includeEngine = builtDepPaths === undefined ||
152
- transitivelyRequiresBuild(graph, builtDepPaths, buildRequiredCache ??= {}, depPath, new Set());
202
+ const includeEngine = buildRequiredDepPaths === undefined || buildRequiredDepPaths.has(depPath);
153
203
  // A snapshot that declares `engines.runtime` carries the desugared
154
204
  // `node@runtime:<version>` pin as a child; that's the Node the bin
155
205
  // linker spawns for its lifecycle scripts, so it has to drive the
@@ -157,7 +207,13 @@ export function calcGraphNodeHash({ graph, cache, builtDepPaths, buildRequiredCa
157
207
  // to the install-wide value.
158
208
  const ownPin = readSnapshotRuntimePin(graph[depPath]?.children);
159
209
  const engine = includeEngine ? engineName(ownPin ?? nodeVersion) : null;
160
- const deps = calcDepGraphHash(graph, cache, new Set(), depPath, supportedArchitectures);
210
+ const deps = calcDepGraphHash({
211
+ depsGraph: graph,
212
+ cache,
213
+ parents: new Set(),
214
+ depPath,
215
+ context: createDepGraphHashContext(supportedArchitectures),
216
+ });
161
217
  const isLocalDirectory = isLocalDirectoryResolution(graph[depPath]?.resolution);
162
218
  // Scoping the slot needs the project's identity; the segment only needs to
163
219
  // know that the package is a local directory, so a caller that leaves
@@ -259,14 +315,16 @@ export function lockfileToDepGraph(lockfile, supportedArchitectures, lockfileDir
259
315
  const linkTargetNodes = new Set();
260
316
  if (lockfile.packages != null) {
261
317
  for (const [depPath, pkgSnapshot] of Object.entries(lockfile.packages)) {
318
+ const pkgIdWithPatchHash = getPkgIdWithPatchHash(depPath);
262
319
  const children = lockfileDepsToGraphChildren({
263
320
  ...pkgSnapshot.dependencies,
264
321
  ...pkgSnapshot.optionalDependencies,
265
322
  }, lockfileDir, linkTargetNodes);
266
323
  graph[depPath] = {
267
324
  children,
325
+ pkgIdWithPatchHash,
268
326
  resolution: pkgSnapshot.resolution,
269
- fullPkgId: createFullPkgId(getPkgIdWithPatchHash(depPath), pkgSnapshot.resolution, supportedArchitectures),
327
+ fullPkgId: createFullPkgId(pkgIdWithPatchHash, pkgSnapshot.resolution, supportedArchitectures),
270
328
  };
271
329
  }
272
330
  }
@@ -287,30 +345,47 @@ function computeBuiltDepPaths(entries, allowBuild) {
287
345
  }
288
346
  return builtDepPaths;
289
347
  }
290
- function transitivelyRequiresBuild(graph, builtDepPaths, cache, depPath, parents) {
291
- if (depPath in cache)
292
- return cache[depPath];
293
- if (builtDepPaths.has(depPath)) {
294
- cache[depPath] = true;
295
- return true;
296
- }
297
- const node = graph[depPath];
298
- if (!node) {
299
- cache[depPath] = false;
300
- return false;
301
- }
302
- if (parents.has(depPath)) {
303
- return false;
348
+ /**
349
+ * Expand `builtDepPaths` to every node that is, or transitively depends
350
+ * on, one of them.
351
+ *
352
+ * The result gates the engine string in {@link calcGraphNodeHash}: only a
353
+ * package that may run a build script — or that depends on one — keeps the
354
+ * engine in its GVS hash. It is computed as one graph-wide reverse closure,
355
+ * so a package inside a dependency cycle gets the same answer no matter
356
+ * which node the hasher reaches it from.
357
+ *
358
+ * A path with no node in `graph` is kept and still marks whatever depends on
359
+ * it: the built set comes from the allowBuild policy rather than the graph,
360
+ * so the two can disagree.
361
+ */
362
+ export function computeBuildRequiredDepPaths(graph, builtDepPaths) {
363
+ const buildRequiredDepPaths = new Set(builtDepPaths);
364
+ if (builtDepPaths.size === 0)
365
+ return buildRequiredDepPaths;
366
+ const parentsByChild = new Map();
367
+ for (const parent of Object.keys(graph)) {
368
+ for (const child of Object.values(graph[parent].children)) {
369
+ const parents = parentsByChild.get(child);
370
+ if (parents == null) {
371
+ parentsByChild.set(child, [parent]);
372
+ }
373
+ else {
374
+ parents.push(parent);
375
+ }
376
+ }
304
377
  }
305
- const nextParents = new Set([...parents, depPath]);
306
- for (const childDepPath of Object.values(node.children)) {
307
- if (transitivelyRequiresBuild(graph, builtDepPaths, cache, childDepPath, nextParents)) {
308
- cache[depPath] = true;
309
- return true;
378
+ const pending = Array.from(builtDepPaths);
379
+ while (pending.length > 0) {
380
+ const child = pending.pop();
381
+ for (const parent of parentsByChild.get(child) ?? []) {
382
+ if (!buildRequiredDepPaths.has(parent)) {
383
+ buildRequiredDepPaths.add(parent);
384
+ pending.push(parent);
385
+ }
310
386
  }
311
387
  }
312
- cache[depPath] = false;
313
- return false;
388
+ return buildRequiredDepPaths;
314
389
  }
315
390
  function lockfileDepsToGraphChildren(deps, lockfileDir, linkTargetNodes) {
316
391
  const children = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/deps.graph-hasher",
3
- "version": "1100.2.18",
3
+ "version": "1100.3.1",
4
4
  "description": "Calculates the state of a dependency",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -28,17 +28,17 @@
28
28
  ],
29
29
  "dependencies": {
30
30
  "@pnpm/crypto.object-hasher": "1100.0.1",
31
- "@pnpm/deps.path": "1101.0.0",
32
- "@pnpm/engine.runtime.system-version": "1100.0.10",
33
- "@pnpm/lockfile.types": "1100.0.20",
34
- "@pnpm/lockfile.utils": "1102.0.0",
35
- "@pnpm/resolving.resolver-base": "1101.1.1",
36
- "@pnpm/types": "1102.0.0",
31
+ "@pnpm/deps.path": "1101.0.1",
32
+ "@pnpm/engine.runtime.system-version": "1100.0.11",
33
+ "@pnpm/lockfile.types": "1100.1.1",
34
+ "@pnpm/lockfile.utils": "1102.1.1",
35
+ "@pnpm/resolving.resolver-base": "1101.2.0",
36
+ "@pnpm/types": "1102.1.0",
37
37
  "detect-libc": "^2.1.2"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@jest/globals": "30.4.1",
41
- "@pnpm/deps.graph-hasher": "1100.2.18"
41
+ "@pnpm/deps.graph-hasher": "1100.3.1"
42
42
  },
43
43
  "engines": {
44
44
  "node": ">=22.13"