@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.
|
|
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.
|
|
8
|
-
2.
|
|
9
|
-
3.
|
|
10
|
-
|
|
11
|
-
|
|
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
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
57
|
+
* Resolve a project against a pnpr server and return the resolved
|
|
58
|
+
* lockfile.
|
|
78
59
|
*
|
|
79
|
-
* `POST /v1/
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
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
|
|
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
|
-
*
|
|
7
|
+
* Resolve a project against a pnpr server and return the resolved
|
|
8
|
+
* lockfile.
|
|
10
9
|
*
|
|
11
|
-
* `POST /v1/
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
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
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
throw new Error(
|
|
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
|
-
|
|
49
|
-
|
|
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(
|
|
73
|
-
stats:
|
|
74
|
-
fileDownloads,
|
|
75
|
-
indexEntries,
|
|
53
|
+
lockfile: convertToLockfileObject(terminal.lockfile),
|
|
54
|
+
stats: terminal.stats,
|
|
76
55
|
};
|
|
77
56
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
|
87
|
+
async function postResolve(registryUrl, body, authorization) {
|
|
108
88
|
const base = registryUrl.endsWith('/') ? registryUrl : `${registryUrl}/`;
|
|
109
|
-
const url = new URL('v1/
|
|
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
|
|
117
|
-
//
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "Client for the pnpr server —
|
|
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/
|
|
31
|
-
"@pnpm/lockfile.
|
|
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.
|
|
41
|
-
"@pnpm/pnpr.client": "1.
|
|
34
|
+
"@pnpm/types": "1101.3.1",
|
|
35
|
+
"@pnpm/pnpr.client": "1.2.0"
|
|
42
36
|
},
|
|
43
37
|
"engines": {
|
|
44
38
|
"node": ">=22.13"
|