@pnpm/pnpr.client 1.1.0 → 1.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/README.md CHANGED
@@ -1,15 +1,14 @@
1
1
  # @pnpm/pnpr.client
2
2
 
3
- Client library for the pnpr server. Reads the local store state, sends it to the server, and writes the received files into the content-addressable store.
3
+ Client library for the pnpr server. Resolves a project's dependencies server-side and returns the resolved lockfile.
4
4
 
5
5
  ## How it works
6
6
 
7
- 1. Reads integrity hashes from the local store index (`index.db`).
8
- 2. Sends `POST /v1/install` to the pnpr server with the project's dependencies and the store integrities.
9
- 3. Parses the NDJSON streaming response `D`-lines (missing file digests) are dispatched to worker downloads against `/v1/files`, `I`-lines are buffered as raw store-index entries, and the final `L`-line yields the resolved lockfile and stats.
10
- 4. File download workers write each received file directly to the local CAFS (`files/{hash[:2]}/{hash[2:]}`).
11
- 5. Writes store index entries for all new packages in a single SQLite transaction.
12
- 6. Returns the resolved lockfile for use with pnpm's headless install (linking phase).
7
+ 1. Sends `POST /v1/install` to the pnpr server with the project's dependencies (and the existing lockfile, if any, for incremental resolution).
8
+ 2. The server resolves against the client's registries, verifies the input lockfile under the client's policy, and answers with one gzipped JSON object carrying the resolved lockfile and stats.
9
+ 3. Returns the resolved lockfile for use with pnpm's headless install, which fetches every tarball directly from the registries in parallel like a normal install. See [pnpm/pnpm#12230](https://github.com/pnpm/pnpm/issues/12230).
10
+
11
+ pnpr is a stateless resolver: it stores no tarballs and serves no file content.
13
12
 
14
13
  ## Usage
15
14
 
@@ -17,20 +16,14 @@ This package is used internally by pnpm when the `pnprServer` config option is s
17
16
 
18
17
  ```typescript
19
18
  import { fetchFromPnpmRegistry } from '@pnpm/pnpr.client'
20
- import { StoreIndex } from '@pnpm/store.index'
21
-
22
- const storeIndex = new StoreIndex('/path/to/store')
23
19
 
24
20
  const { lockfile, stats } = await fetchFromPnpmRegistry({
25
21
  registryUrl: 'http://localhost:4000',
26
- storeDir: '/path/to/store',
27
- storeIndex,
28
22
  dependencies: { react: '^19.0.0' },
29
23
  devDependencies: { typescript: '^5.0.0' },
30
24
  })
31
25
 
32
26
  console.log(`Resolved ${stats.totalPackages} packages`)
33
- console.log(`${stats.alreadyInStore} cached, ${stats.filesToDownload} files downloaded`)
34
27
  // lockfile is ready for headless install
35
28
  ```
36
29
 
package/lib/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { fetchFromPnpmRegistry, type FetchFromPnpmRegistryOptions, type FetchFromPnpmRegistryResult, writeRawIndexEntries } from './fetchFromPnpmRegistry.js';
2
1
  export { type ResponseMetadata } from './protocol.js';
2
+ export { resolveViaPnprServer, type ResolveViaPnprServerOptions, type ResolveViaPnprServerResult } from './resolveViaPnprServer.js';
package/lib/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { fetchFromPnpmRegistry, writeRawIndexEntries } from './fetchFromPnpmRegistry.js';
2
1
  export {} from './protocol.js';
2
+ export { resolveViaPnprServer } from './resolveViaPnprServer.js';
3
3
  //# sourceMappingURL=index.js.map
package/lib/protocol.d.ts CHANGED
@@ -3,11 +3,5 @@ export interface ResponseMetadata {
3
3
  lockfile: LockfileObject;
4
4
  stats: {
5
5
  totalPackages: number;
6
- alreadyInStore: number;
7
- packagesToFetch: number;
8
- filesInNewPackages: number;
9
- filesAlreadyInCafs: number;
10
- filesToDownload: number;
11
- downloadBytes: number;
12
6
  };
13
7
  }
@@ -1,5 +1,4 @@
1
1
  import type { LockfileFile, LockfileObject } from '@pnpm/lockfile.types';
2
- import { StoreIndex } from '@pnpm/store.index';
3
2
  import type { ResponseMetadata } from './protocol.js';
4
3
  export interface PnprProject {
5
4
  /** Relative dir within the workspace (e.g. "." or "packages/foo") */
@@ -8,13 +7,9 @@ export interface PnprProject {
8
7
  devDependencies?: Record<string, string>;
9
8
  optionalDependencies?: Record<string, string>;
10
9
  }
11
- export interface FetchFromPnpmRegistryOptions {
10
+ export interface ResolveViaPnprServerOptions {
12
11
  /** URL of the pnpr server */
13
12
  registryUrl: string;
14
- /** Client's store directory */
15
- storeDir: string;
16
- /** Client's store index */
17
- storeIndex: StoreIndex;
18
13
  /** Dependencies to resolve (single project) */
19
14
  dependencies?: Record<string, string>;
20
15
  /** Dev dependencies to resolve (single project) */
@@ -32,13 +27,13 @@ export interface FetchFromPnpmRegistryOptions {
32
27
  namedRegistries?: Record<string, string>;
33
28
  /**
34
29
  * The caller's forwarded upstream credentials, keyed by nerf-darted
35
- * registry URI, so the server resolves/fetches private content as the
30
+ * registry URI, so the server resolves private content as the
36
31
  * caller. Distinct from `authorization` (pnpr identity).
37
32
  */
38
33
  authHeaders?: Record<string, string>;
39
34
  /**
40
35
  * `Authorization` for the pnpr server's own URL (`undefined` if none):
41
- * identifies the caller to pnpr's gate and keys the grant table.
36
+ * identifies the caller to pnpr's gate.
42
37
  */
43
38
  authorization?: string;
44
39
  /** Overrides */
@@ -53,37 +48,20 @@ export interface FetchFromPnpmRegistryOptions {
53
48
  * `readWantedLockfileFile` so no in-memory→on-disk round-trip is needed.
54
49
  */
55
50
  lockfile?: LockfileFile;
56
- /**
57
- * `--lockfile-only`: resolve and return only the lockfile — fetch no
58
- * files into the local store. Forwarded to the server (which skips the
59
- * file diff); the client ignores the (empty) file payload so the store
60
- * stays untouched. Mirrors pnpm's resolve + write, fetch nothing, link
61
- * nothing. See https://github.com/pnpm/pnpm/issues/12146.
62
- */
63
- lockfileOnly?: boolean;
64
51
  }
65
- export interface FetchFromPnpmRegistryResult {
52
+ export interface ResolveViaPnprServerResult {
66
53
  lockfile: LockfileObject;
67
54
  stats: ResponseMetadata['stats'];
68
- /** Promise that resolves when all file downloads are written to CAFS */
69
- fileDownloads: Promise<void>;
70
- /** Pre-packed store index entries to write to SQLite */
71
- indexEntries: Array<{
72
- key: string;
73
- buffer: Uint8Array;
74
- }>;
75
55
  }
76
56
  /**
77
- * Fetch resolved dependencies from a pnpr server in a single round trip.
57
+ * Resolve a project against a pnpr server and return the resolved
58
+ * lockfile.
78
59
  *
79
- * `POST /v1/install` (with `inlineFiles`) answers with one gzipped binary
80
- * body: a length-prefixed JSON header (lockfile, stats, store-index
81
- * entries, or verification violations) followed by the missing files'
82
- * contents as binary frames. We parse the header here and hand the file
83
- * frames to a worker that writes them straight into the CAFS.
60
+ * `POST /v1/resolve` answers with an `application/x-ndjson` stream: one
61
+ * `package` frame per resolved tarball as the server's tree walk yields
62
+ * it, then exactly one terminal frame `done` (full lockfile + stats),
63
+ * `error`, or `violations`. pnpr serves no file content the caller
64
+ * fetches every tarball itself, in parallel, like a normal install
65
+ * ([pnpm/pnpm#12230](https://github.com/pnpm/pnpm/issues/12230)).
84
66
  */
85
- export declare function fetchFromPnpmRegistry(opts: FetchFromPnpmRegistryOptions): Promise<FetchFromPnpmRegistryResult>;
86
- export declare function writeRawIndexEntries(indexEntries: Array<{
87
- key: string;
88
- buffer: Uint8Array;
89
- }>, storeIndex: StoreIndex): void;
67
+ export declare function resolveViaPnprServer(opts: ResolveViaPnprServerOptions): Promise<ResolveViaPnprServerResult>;
@@ -3,19 +3,18 @@ import https from 'node:https';
3
3
  import { URL } from 'node:url';
4
4
  import { gunzip } from 'node:zlib';
5
5
  import { convertToLockfileObject } from '@pnpm/lockfile.fs';
6
- import { StoreIndex } from '@pnpm/store.index';
7
- import { writeCafsFiles } from '@pnpm/worker';
8
6
  /**
9
- * Fetch resolved dependencies from a pnpr server in a single round trip.
7
+ * Resolve a project against a pnpr server and return the resolved
8
+ * lockfile.
10
9
  *
11
- * `POST /v1/install` (with `inlineFiles`) answers with one gzipped binary
12
- * body: a length-prefixed JSON header (lockfile, stats, store-index
13
- * entries, or verification violations) followed by the missing files'
14
- * contents as binary frames. We parse the header here and hand the file
15
- * frames to a worker that writes them straight into the CAFS.
10
+ * `POST /v1/resolve` answers with an `application/x-ndjson` stream: one
11
+ * `package` frame per resolved tarball as the server's tree walk yields
12
+ * it, then exactly one terminal frame `done` (full lockfile + stats),
13
+ * `error`, or `violations`. pnpr serves no file content the caller
14
+ * fetches every tarball itself, in parallel, like a normal install
15
+ * ([pnpm/pnpm#12230](https://github.com/pnpm/pnpm/issues/12230)).
16
16
  */
17
- export async function fetchFromPnpmRegistry(opts) {
18
- const storeIntegrities = readStoreIntegrities(opts.storeIndex);
17
+ export async function resolveViaPnprServer(opts) {
19
18
  const projects = opts.projects ?? [{
20
19
  dir: '.',
21
20
  dependencies: opts.dependencies,
@@ -36,85 +35,66 @@ export async function fetchFromPnpmRegistry(opts) {
36
35
  // protocol carries (split `packages`/`snapshots`, `{ specifier, version }`
37
36
  // importer deps).
38
37
  lockfile: opts.lockfile,
39
- lockfileOnly: opts.lockfileOnly,
40
- storeIntegrities,
41
- inlineFiles: true,
42
38
  });
43
- const body = await postInstall(opts.registryUrl, requestBody, opts.authorization);
44
- // The combined response is `[u32 header length][header JSON][file frames]`.
45
- if (body.length < 4) {
46
- throw new Error('pnpr server returned a truncated /v1/install response');
39
+ const body = await postResolve(opts.registryUrl, requestBody, opts.authorization);
40
+ const terminal = parseTerminalFrame(body.toString('utf-8'));
41
+ if (terminal.type === 'error') {
42
+ throw new Error(terminal.message);
47
43
  }
48
- const headerLength = body.readUInt32BE(0);
49
- const header = JSON.parse(body.subarray(4, 4 + headerLength).toString('utf-8'));
50
- if (header.violations != null && header.violations.length > 0) {
51
- const rendered = header.violations
44
+ if (terminal.type === 'violations') {
45
+ const rendered = terminal.violations
52
46
  .map((violation) => ` ${violation.name}@${violation.version}: ${violation.reason}`)
53
47
  .join('\n');
54
48
  throw new Error(`pnpr server rejected the lockfile under the verification policy:\n${rendered}`);
55
49
  }
56
- const indexEntries = (header.indexEntries ?? []).map(({ key, b64 }) => ({
57
- key,
58
- buffer: new Uint8Array(Buffer.from(b64, 'base64')),
59
- }));
60
- // `--lockfile-only` fetches nothing: there are no file frames to write
61
- // (the server sends only the end-of-stream marker), so leave the store
62
- // untouched.
63
- const fileDownloads = opts.lockfileOnly
64
- ? Promise.resolve()
65
- : writeCafsFiles({
66
- storeDir: opts.storeDir,
67
- payload: body.subarray(4 + headerLength),
68
- }).then(() => { });
69
50
  return {
70
51
  // The server speaks the on-disk lockfile format; convert it to the
71
52
  // in-memory `LockfileObject` the rest of pnpm consumes.
72
- lockfile: convertToLockfileObject(header.lockfile),
73
- stats: header.stats,
74
- fileDownloads,
75
- indexEntries,
53
+ lockfile: convertToLockfileObject(terminal.lockfile),
54
+ stats: terminal.stats,
76
55
  };
77
56
  }
78
- function readStoreIntegrities(storeIndex) {
79
- const seen = new Set();
80
- for (const key of storeIndex.keys()) {
81
- const tabIdx = key.indexOf('\t');
82
- if (tabIdx === -1)
57
+ /**
58
+ * Parse the NDJSON `/v1/resolve` body and return its single terminal
59
+ * frame. `package` frames are skipped — this client fetches tarballs the
60
+ * normal way after resolution rather than overlapping fetch with the
61
+ * stream. Throws on an unknown frame type (so a protocol mismatch fails
62
+ * fast here rather than as a confusing lockfile error downstream) or if
63
+ * the stream carries no terminal frame.
64
+ */
65
+ function parseTerminalFrame(body) {
66
+ for (const line of body.split('\n')) {
67
+ if (line.trim() === '')
83
68
  continue;
84
- const integrity = key.slice(0, tabIdx);
85
- // StoreIndex also stores non-integrity keys (e.g. git-hosted entries
86
- // keyed by URL). Filter to actual SRI hashes — sending those over to
87
- // the pnpr server would just bloat the request without ever matching.
88
- if (!isIntegrityLike(integrity))
69
+ const frame = JSON.parse(line);
70
+ if (frame.type === 'package')
89
71
  continue;
90
- seen.add(integrity);
72
+ if (frame.type === 'done' || frame.type === 'error' || frame.type === 'violations') {
73
+ return frame;
74
+ }
75
+ throw new Error(`pnpr server /v1/resolve stream emitted an unknown frame type: ${String(frame.type)}`);
91
76
  }
92
- return [...seen];
93
- }
94
- function isIntegrityLike(value) {
95
- return value.startsWith('sha512-') ||
96
- value.startsWith('sha256-') ||
97
- value.startsWith('sha1-');
77
+ throw new Error('pnpr server /v1/resolve stream ended without a terminal frame');
98
78
  }
99
79
  const REQUEST_TIMEOUT = 600_000; // 10 minutes — server-side resolution can be slow on first run
100
80
  /**
101
- * `POST /v1/install` and return the full response body, decompressed.
81
+ * `POST /v1/resolve` and return the full response body, decompressed.
102
82
  *
103
83
  * `urlPath` resolution normalizes the base to end with "/" so a path
104
84
  * prefix configured on the pnpr server URL (e.g. https://host/pnpr/) is
105
85
  * preserved.
106
86
  */
107
- async function postInstall(registryUrl, body, authorization) {
87
+ async function postResolve(registryUrl, body, authorization) {
108
88
  const base = registryUrl.endsWith('/') ? registryUrl : `${registryUrl}/`;
109
- const url = new URL('v1/install', base);
89
+ const url = new URL('v1/resolve', base);
110
90
  const requestFn = url.protocol === 'https:' ? https.request : http.request;
111
91
  const headers = {
112
92
  'Content-Type': 'application/json',
113
93
  'Content-Length': Buffer.byteLength(body),
114
94
  'Accept-Encoding': 'gzip',
115
95
  };
116
- // Identify the caller to the pnpr server's access gate so protected
117
- // packages resolve and the per-user grant table keys on the right user.
96
+ // Identify the caller to the pnpr server so private packages resolve
97
+ // with the right credentials.
118
98
  if (authorization != null) {
119
99
  headers.Authorization = authorization;
120
100
  }
@@ -170,10 +150,4 @@ async function postInstall(registryUrl, body, authorization) {
170
150
  req.end();
171
151
  });
172
152
  }
173
- export function writeRawIndexEntries(indexEntries, storeIndex) {
174
- const writes = indexEntries.filter(({ key }) => !storeIndex.has(key));
175
- if (writes.length > 0) {
176
- storeIndex.setRawMany(writes);
177
- }
178
- }
179
- //# sourceMappingURL=fetchFromPnpmRegistry.js.map
153
+ //# sourceMappingURL=resolveViaPnprServer.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pnpm/pnpr.client",
3
- "version": "1.1.0",
4
- "description": "Client for the pnpr server — sends store state, receives resolved lockfile and missing files",
3
+ "version": "1.2.0",
4
+ "description": "Client for the pnpr server — resolves a project server-side and receives the resolved lockfile",
5
5
  "keywords": [
6
6
  "pnpm",
7
7
  "pnpm11"
@@ -27,18 +27,12 @@
27
27
  "!*.map"
28
28
  ],
29
29
  "dependencies": {
30
- "@pnpm/store.index": "1100.1.0",
31
- "@pnpm/lockfile.fs": "1100.1.3",
32
- "@pnpm/store.cafs": "1100.1.8",
33
- "@pnpm/lockfile.types": "1100.0.9"
34
- },
35
- "peerDependencies": {
36
- "@pnpm/logger": "^1001.0.1",
37
- "@pnpm/worker": "^1100.1.9"
30
+ "@pnpm/lockfile.fs": "1100.1.4",
31
+ "@pnpm/lockfile.types": "1100.0.10"
38
32
  },
39
33
  "devDependencies": {
40
- "@pnpm/types": "1101.3.0",
41
- "@pnpm/pnpr.client": "1.1.0"
34
+ "@pnpm/types": "1101.3.1",
35
+ "@pnpm/pnpr.client": "1.2.0"
42
36
  },
43
37
  "engines": {
44
38
  "node": ">=22.13"