Files
pnpm/agent/server
Zoltan Kochan 4d7cd56ccc chore: upgrade @typescript/native-preview to 7.0.0-dev.20260421.2 (#11332)
* chore: upgrade @typescript/native-preview to 7.0.0-dev.20260421.2

- Add explicit `types: ["node"]` to the shared tsconfig because tsgo
  20260421 no longer auto-acquires `@types/*` from `node_modules`.
- Refactor test files to explicitly import jest globals (`describe`,
  `it`, `test`, `expect`, `beforeEach`, etc.) from `@jest/globals`
  instead of relying on `@types/jest` ambient declarations. Under the
  new tsgo build, `import { jest } from '@jest/globals'` shadows the
  ambient `jest` namespace, breaking `@types/jest`'s `declare var
  describe: jest.Describe;` globals.
- Add `@jest/globals` to each package's devDependencies where tests
  now import from it, and add `@types/node` to packages that need it
  but were relying on hoisted resolution.
- Replace `fail()` calls with `throw new Error(...)` since `fail` is
  no longer globally available.

* chore: fix remaining tsgo type-strictness errors

- Strip `as <PnpmType>` casts on objects passed to toMatchObject /
  toStrictEqual / toEqual; @jest/globals rejects the typed objects
  (which include AsymmetricMatchers) vs. the repo-specific type.
- Type `jest.fn<...>()` explicitly where the mock's signature matters
  for toHaveBeenCalledWith.
- Replace `beforeEach(() => X)` with `beforeEach(() => { X })` so the
  return value is void, as the stricter jest typing requires.
- Use `expect.objectContaining({...})` in one place where the full
  expected object triggered stricter type resolution.
- Cast `prompt.mock.calls` arg through `as unknown as Record<...>[]`
  for patch.test.ts's nested-array matchers.
- Fix off-by-one `<reference path>` in pnpm/test/getConfig.test.ts
  that only surfaced now.
- Move `@jest/globals` from devDependencies to dependencies in the
  two `__utils__` packages that import it from `src/`.
- Clean up unused imports from the @jest/globals migration.

* chore: address Copilot review on #11332

- Move misplaced `@jest/globals` imports to the top import block in
  checkEngine, run.ts, and workspace/root-finder tests where the
  script dropped them below executable code.
- Replace `try { await x(); throw new Error('should have thrown') } catch`
  in bins/linker, lockfile/fs, and resolving/local-resolver tests with
  `await expect(x()).rejects.toMatchObject({...})`. The old pattern
  swallowed an unrelated `throw` if the under-test call silently
  succeeded, which would fail on the catch-block assertion with a
  misleading message.
2026-04-21 23:21:52 +02:00
..
2026-04-21 15:03:02 +02:00
2026-04-21 15:03:02 +02:00

pnpm-agent

A pnpm agent server that resolves dependencies server-side and streams only the files missing from the client's content-addressable store.

Status: experimental. Versions are pre-1.0; the wire protocol may change between releases.

How it works

  1. Client sends POST /v1/install with dependencies, an optional existing lockfile, and the integrity hashes of packages already in its store.
  2. Server resolves the full dependency tree using pnpm's own resolution engine.
  3. Server computes which file digests the client is missing — at the individual file level, not just the package level.
  4. Server streams an NDJSON response on /v1/install (D-lines for missing file digests, I-lines for pre-packed package index entries, a final L-line with the lockfile + stats, or an E-line on error).
  5. The client then requests the missing file contents from POST /v1/files, which streams a gzip-compressed binary of packed file entries.

This eliminates sequential metadata round-trips (the server resolves in one shot) and avoids downloading files that already exist in the client's store from other packages.

Starting the server

Install from npm

pnpm add -g pnpm-agent
pnpm-agent

Docker

A Dockerfile is provided at agent/server/Dockerfile. It is layered on top of ghcr.io/pnpm/pnpm and installs Node.js and pnpm-agent inside the image.

# Build the image locally
docker build -t pnpm-agent agent/server

# Run it, persisting the store + cache in ./agent-data
docker run --rm \
  -p 4873:4873 \
  -v "$(pwd)/agent-data:/agent-data" \
  pnpm-agent

Override the defaults with -e, same variables as described below:

docker run --rm \
  -p 4000:4000 \
  -e PORT=4000 \
  -e PNPM_AGENT_UPSTREAM=https://my-proxy.example.com/ \
  -v "$(pwd)/agent-data:/agent-data" \
  pnpm-agent

The image exposes port 4873 and declares a /agent-data volume; mount a host directory there if you want the resolved metadata, store index, and file store to survive container restarts.

From source

# Build first
pnpm --filter pnpm-agent run compile

# Run with defaults (port 4873, upstream https://registry.npmjs.org/)
node lib/bin.js

# Or configure via environment variables
PORT=4000 \
PNPM_AGENT_STORE_DIR=./my-store \
PNPM_AGENT_CACHE_DIR=./my-cache \
PNPM_AGENT_UPSTREAM=https://registry.npmjs.org/ \
node lib/bin.js

Environment variables

Variable Default Description
PORT 4873 Port to listen on
PNPM_AGENT_STORE_DIR ./store Directory for the server's content-addressable store
PNPM_AGENT_CACHE_DIR ./cache Directory for package metadata cache
PNPM_AGENT_UPSTREAM https://registry.npmjs.org/ Upstream npm registry to resolve from

Programmatic usage

import { createRegistryServer } from 'pnpm-agent'

const server = await createRegistryServer({
  storeDir: '/var/lib/pnpm-agent/store',
  cacheDir: '/var/lib/pnpm-agent/cache',
  registries: { default: 'https://registry.npmjs.org/' },
})

server.listen(4000, () => {
  console.log('pnpm agent listening on port 4000')
})

Quick start

Terminal 1 — start the server:

cd agent/server
pnpm run compile
node lib/bin.js
# pnpm agent server listening on http://localhost:4873

Terminal 2 — use it from any project:

cd my-project

Add to pnpm-workspace.yaml:

agent: http://localhost:4873

Or pass --config.agent=http://localhost:4873 on the command line.

Then run:

pnpm install

That's it. pnpm will resolve dependencies on the server, download only the files missing from your local store, and link node_modules as usual. Remove the agent setting to go back to normal behavior.

API

POST /v1/install

Request body (JSON):

{
  "projects": [
    {
      "dir": ".",
      "dependencies": { "react": "^19.0.0" },
      "devDependencies": { "typescript": "^5.0.0" }
    }
  ],
  "overrides": {},
  "lockfile": null,
  "storeIntegrities": ["sha512-abc...", "sha512-def..."]
}

Response (NDJSON, Content-Type: application/x-ndjson). Each line is one message:

  • D\t{digest}\t{size}\t{executable} — file digest missing from the client's store.
  • I\t{integrity}\t{pkgId}\t{base64-msgpack} — pre-packed package index entry.
  • L\t{json} — final lockfile and stats. Emitted last on success.
  • E\t{json} — error. Emitted if resolution fails.

POST /v1/files

Request body (JSON):

{ "digests": [{ "digest": "<hex>", "size": 123, "executable": false }] }

Response (gzip-compressed binary, Content-Type: application/x-pnpm-install):

[4 bytes: JSON metadata length]
[N bytes: JSON metadata]
[file entries: 64B digest + 4B size + 1B mode + content, repeated]
[64 zero bytes: end marker]