From a017bf3394adeabc22a538606ab1cccaad94c29a Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Wed, 3 Jun 2026 12:01:48 +0200 Subject: [PATCH] refactor: rename the agent client and `agent` setting to pnpr (#12155) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: rename the agent client + setting to pnpr The pnpm-side client and its config setting still carried the old "agent" name after the server moved to pnpr. Align both with pnpr (and with pacquet, which already uses `pnprServer`): - Move `agent/client` → `pnpr/client` and rename the package `@pnpm/agent.client` → `@pnpm/pnpr.client` (exported `AgentProject` type → `PnprProject`). - Rename the config setting `agent` → `pnprServer` (`--pnpr-server` CLI flag), matching pacquet's setting name. - Rename the internal install-path symbols and the user-facing log / error strings that mentioned "pnpm agent" to "pnpr". No behavioral change — only names. The e2e suite now drives `--config.pnprServer`. * fix: forward optionalDependencies to the pnpr server `PnprProject` and the install-request body only carried `dependencies` and `devDependencies`, so a project's `optionalDependencies` were dropped on the way to the pnpr server — it resolved as if they didn't exist, producing a different lockfile than the local resolver. Thread `optionalDependencies` through the client request shape, the deps-installer single-project and workspace request builders, and the pnpr server (`InstallRequestProject` / `InstallRequest` + the throwaway manifest it writes for resolution). Adds an e2e case asserting an optional dependency is resolved through `pnprServer`. --- .changeset/pnpr-forward-optional-deps.md | 7 + .changeset/pnpr-lockfile-only.md | 4 +- .changeset/rename-agent-to-pnpr-server.md | 10 ++ .meta-updater/src/index.ts | 2 +- config/reader/src/Config.ts | 2 +- config/reader/src/configFileKey.ts | 2 +- .../reader/src/getOptionsFromRootManifest.ts | 2 +- config/reader/src/types.ts | 2 +- core/types/src/package.ts | 2 +- installing/commands/src/install.ts | 4 +- installing/commands/src/installDeps.ts | 2 +- installing/commands/src/recursive.ts | 2 +- installing/deps-installer/package.json | 2 +- .../src/install/extendInstallOptions.ts | 7 +- .../deps-installer/src/install/index.ts | 122 +++++++++--------- installing/deps-installer/tsconfig.json | 6 +- pacquet/crates/pnpr-client/src/lib.rs | 2 +- pnpm-lock.yaml | 62 ++++----- pnpm-workspace.yaml | 2 +- pnpm/test/install/pnpmRegistry.ts | 75 +++++++---- {agent => pnpr}/client/.gitignore | 0 {agent => pnpr}/client/CHANGELOG.md | 0 {agent => pnpr}/client/README.md | 12 +- {agent => pnpr}/client/package.json | 10 +- .../client/src/fetchFromPnpmRegistry.ts | 26 ++-- {agent => pnpr}/client/src/index.ts | 0 {agent => pnpr}/client/src/protocol.ts | 0 {agent => pnpr}/client/tsconfig.json | 0 {agent => pnpr}/client/tsconfig.lint.json | 0 .../pnpr/src/install_accelerator/protocol.rs | 9 +- .../pnpr/src/install_accelerator/resolve.rs | 1 + worker/src/index.ts | 2 +- worker/src/start.ts | 20 +-- 33 files changed, 226 insertions(+), 173 deletions(-) create mode 100644 .changeset/pnpr-forward-optional-deps.md create mode 100644 .changeset/rename-agent-to-pnpr-server.md rename {agent => pnpr}/client/.gitignore (100%) rename {agent => pnpr}/client/CHANGELOG.md (100%) rename {agent => pnpr}/client/README.md (70%) rename {agent => pnpr}/client/package.json (74%) rename {agent => pnpr}/client/src/fetchFromPnpmRegistry.ts (91%) rename {agent => pnpr}/client/src/index.ts (100%) rename {agent => pnpr}/client/src/protocol.ts (100%) rename {agent => pnpr}/client/tsconfig.json (100%) rename {agent => pnpr}/client/tsconfig.lint.json (100%) diff --git a/.changeset/pnpr-forward-optional-deps.md b/.changeset/pnpr-forward-optional-deps.md new file mode 100644 index 0000000000..98df872328 --- /dev/null +++ b/.changeset/pnpr-forward-optional-deps.md @@ -0,0 +1,7 @@ +--- +"@pnpm/pnpr.client": patch +"@pnpm/installing.deps-installer": patch +"pnpm": patch +--- + +Fixed `optionalDependencies` being dropped when resolving through a `pnprServer`. The pnpr request now carries each project's optional dependencies (for both single-project and workspace installs), so the server resolves them like the local resolver does instead of producing a lockfile as if they did not exist. diff --git a/.changeset/pnpr-lockfile-only.md b/.changeset/pnpr-lockfile-only.md index 970807df75..ef0ae14de3 100644 --- a/.changeset/pnpr-lockfile-only.md +++ b/.changeset/pnpr-lockfile-only.md @@ -1,7 +1,7 @@ --- -"@pnpm/agent.client": minor +"@pnpm/pnpr.client": minor "@pnpm/installing.deps-installer": patch "pnpm": patch --- -`pnpm install --lockfile-only` (and the `lockfileOnly` setting) is now honored when an agent / `pnprServer` is configured. The agent path resolves and writes `pnpm-lock.yaml` but fetches no files into the store and links no `node_modules`, matching the local lockfile-only behavior. The client ignores any file/index lines an older agent server still streams, so the store stays untouched even against a server that predates the resolve-only mode [#12146](https://github.com/pnpm/pnpm/issues/12146). +`pnpm install --lockfile-only` (and the `lockfileOnly` setting) is now honored when a `pnprServer` is configured. The pnpr path resolves and writes `pnpm-lock.yaml` but fetches no files into the store and links no `node_modules`, matching the local lockfile-only behavior. The client ignores any file/index lines an older pnpr server still streams, so the store stays untouched even against a server that predates the resolve-only mode [#12146](https://github.com/pnpm/pnpm/issues/12146). diff --git a/.changeset/rename-agent-to-pnpr-server.md b/.changeset/rename-agent-to-pnpr-server.md new file mode 100644 index 0000000000..5dc052b896 --- /dev/null +++ b/.changeset/rename-agent-to-pnpr-server.md @@ -0,0 +1,10 @@ +--- +"@pnpm/pnpr.client": minor +"@pnpm/config.reader": minor +"@pnpm/types": minor +"@pnpm/installing.deps-installer": patch +"@pnpm/installing.commands": patch +"pnpm": minor +--- + +Renamed the experimental `agent` setting to `pnprServer` so the pnpm CLI matches the same setting name pacquet uses for offloading resolution to a [pnpr](https://github.com/pnpm/pnpm/tree/main/pnpr) server. Point pnpm at a pnpr server with `pnprServer: ` in `pnpm-workspace.yaml` (or `--pnpr-server `); the previous `agent` / `--agent` name no longer works. The client package was likewise renamed from `@pnpm/agent.client` to `@pnpm/pnpr.client`. diff --git a/.meta-updater/src/index.ts b/.meta-updater/src/index.ts index 3116e60bae..a0b7e52a75 100644 --- a/.meta-updater/src/index.ts +++ b/.meta-updater/src/index.ts @@ -18,7 +18,7 @@ const CLI_PKG_NAME = 'pnpm' // Experimental packages that are versioned independently on the 0.0.x track // and should not be normalized to the pnpm major version. const EXPERIMENTAL_PKGS = new Set([ - '@pnpm/agent.client', + '@pnpm/pnpr.client', ]) // Files that must be packed with mode 0755 in both `pnpm` and `@pnpm/exe`. diff --git a/config/reader/src/Config.ts b/config/reader/src/Config.ts index a68b0e287e..2b7397ffb9 100644 --- a/config/reader/src/Config.ts +++ b/config/reader/src/Config.ts @@ -223,7 +223,7 @@ export interface Config extends OptionsFromRootManifest { packGzipLevel?: number blockExoticSubdeps?: boolean - agent?: string + pnprServer?: string registries: Registries namedRegistries?: Record diff --git a/config/reader/src/configFileKey.ts b/config/reader/src/configFileKey.ts index 95a6681375..a26a1b6daf 100644 --- a/config/reader/src/configFileKey.ts +++ b/config/reader/src/configFileKey.ts @@ -8,7 +8,6 @@ type PnpmKey = keyof typeof pnpmTypes * Keys from {@link pnpmTypes} that are valid fields in a global config file. */ export const pnpmConfigFileKeys = [ - 'agent', 'bail', 'ci', 'color', @@ -47,6 +46,7 @@ export const pnpmConfigFileKeys = [ 'npm-path', 'npmrc-auth-file', 'package-import-method', + 'pnpr-server', 'prefer-frozen-lockfile', 'prefer-offline', 'prefer-symlinked-executables', diff --git a/config/reader/src/getOptionsFromRootManifest.ts b/config/reader/src/getOptionsFromRootManifest.ts index 3df0526371..dd200c86d4 100644 --- a/config/reader/src/getOptionsFromRootManifest.ts +++ b/config/reader/src/getOptionsFromRootManifest.ts @@ -23,7 +23,7 @@ export type OptionsFromRootManifest = { supportedArchitectures?: SupportedArchitectures allowBuilds?: Record requiredScripts?: string[] -} & Pick +} & Pick export function getOptionsFromPnpmSettings (manifestDir: string | undefined, pnpmSettings: PnpmSettings, manifest?: ProjectManifest): OptionsFromRootManifest { const settings: OptionsFromRootManifest = replaceEnvInSettings(pnpmSettings) diff --git a/config/reader/src/types.ts b/config/reader/src/types.ts index 1869b6d395..6a0495bedd 100644 --- a/config/reader/src/types.ts +++ b/config/reader/src/types.ts @@ -142,7 +142,7 @@ export const pnpmTypes = { 'embed-readme': Boolean, 'skip-manifest-obfuscation': Boolean, 'update-notifier': Boolean, - 'agent': [null, String], + 'pnpr-server': [null, String], 'registry-supports-time-field': Boolean, 'fail-if-no-match': Boolean, 'sync-injected-deps-after-scripts': Array, diff --git a/core/types/src/package.ts b/core/types/src/package.ts index cf929aff4c..8c5b31eb76 100644 --- a/core/types/src/package.ts +++ b/core/types/src/package.ts @@ -200,7 +200,7 @@ export interface PnpmSettings { httpProxy?: string httpsProxy?: string noProxy?: string | boolean - agent?: string + pnprServer?: string } export interface ProjectManifest extends BaseManifest { diff --git a/installing/commands/src/install.ts b/installing/commands/src/install.ts index d56a54af16..1167d6b4a3 100644 --- a/installing/commands/src/install.ts +++ b/installing/commands/src/install.ts @@ -48,8 +48,8 @@ export function rcOptionsTypes (): Record { 'node-linker', 'noproxy', 'package-import-method', - 'agent', 'pnpmfile', + 'pnpr-server', 'prefer-frozen-lockfile', 'prefer-offline', 'production', @@ -350,7 +350,7 @@ export type InstallCommandOptions = Pick & Pick { const rootDir = (opts.dir ?? process.cwd()) as ProjectRootDir - // When a pnpm agent is configured, use server-side resolution + // When a pnpr server is configured, use server-side resolution // instead of the normal resolution flow. - if (opts.agent) { + if (opts.pnprServer) { return installFromPnpmRegistry(manifest, rootDir, opts) } @@ -305,13 +305,13 @@ export async function mutateModules ( const opts = extendOptions(maybeOpts) - // When a pnpm agent is configured, use server-side resolution. The agent + // When a pnpr server is configured, use server-side resolution. The pnpr server // path supports `install`, `installSome` (pnpm add), and `uninstallSome` // (pnpm remove). Mutations that need full client-side resolution (update // flags) still fall through to the normal flow. - if (opts.agent && canUseAgentForMutations(projects)) { - const agentResult = await mutateModulesViaAgent(projects, opts) - if (agentResult) return agentResult + if (opts.pnprServer && canUsePnprForMutations(projects)) { + const pnprResult = await mutateModulesViaPnpr(projects, opts) + if (pnprResult) return pnprResult } const allowBuild = createAllowBuildFunction(opts) @@ -1869,7 +1869,7 @@ function allMutationsAreInstalls (projects: MutatedProject[]): boolean { * Run the pacquet binary if it's configured, otherwise run the JS * `headlessInstall`. Callers can hand off any code path that materializes * an already-resolved lockfile (workspace partial install, hoisted - * linker, agent-server install, frozen install) without restating the + * linker, pnpr server install, frozen install) without restating the * delegation choice. * * Pacquet reads the wanted lockfile from disk and produces its own @@ -1886,7 +1886,7 @@ async function materializeOrDelegate ( ): Promise<{ stats?: InstallationResultStats, ignoredBuilds?: IgnoredBuilds }> { if (opts.runPacquet != null) { // Reached only from the resolve-then-materialize call sites - // (workspace-partial, hoisted-linker, agent install). Each ran a + // (workspace-partial, hoisted-linker, pnpr server install). Each ran a // lockfileOnly resolve pass that emitted one // `pnpm:progress status:resolved` per package, so pacquet's // duplicate `resolved` events would double the reporter's count. @@ -2072,13 +2072,13 @@ function getProjectsWithTargetDirs ( } /** - * Whether the agent path can handle this batch of mutations. The agent flow + * Whether the pnpr server path can handle this batch of mutations. The pnpr server flow * supports installing the manifest as-is (`install`), adding new deps * (`installSome`), and removing deps (`uninstallSome`). It cannot model the * client-side update-flag behavior (`update`/`updateMatching`/`updateToLatest`) * yet, so those still go through the normal client-side resolver. */ -function canUseAgentForMutations (projects: MutatedProject[]): boolean { +function canUsePnprForMutations (projects: MutatedProject[]): boolean { if (projects.length === 0) return false return projects.every((p) => { if (p.mutation === 'uninstallSome') return true @@ -2088,52 +2088,52 @@ function canUseAgentForMutations (projects: MutatedProject[]): boolean { }) } -interface AgentNewDep { +interface PnprNewDep { alias: string /** * Whether the user specified a spec (e.g. `pnpm add foo@^2`). If true, the * manifest already has the right value and we must preserve it. If false * we merged in `'latest'` and need to compute a save-prefix spec from the - * resolved version in the lockfile after the agent runs. + * resolved version in the lockfile after the pnpr server runs. */ userSpecified: boolean } -interface AgentInstallProject { +interface PnprInstallProject { rootDir: ProjectRootDir - /** The (possibly pre-processed) manifest we send to the agent. */ + /** The (possibly pre-processed) manifest we send to the pnpr server. */ manifest: ProjectManifest mutation: MutatedProject['mutation'] /** Newly added deps from an `installSome` mutation. Empty otherwise. */ - newDeps: AgentNewDep[] + newDeps: PnprNewDep[] /** Save-prefix config for `installSome`; applied to deps whose spec defaulted to `'latest'`. */ pinnedVersion?: PinnedVersion } /** - * Pre-process projects for the agent flow: + * Pre-process projects for the pnpr server flow: * - `install`: send the manifest as-is. * - `uninstallSome`: drop the named deps from the manifest before sending, - * so the agent's resolution naturally produces a lockfile without them. + * so the pnpr server's resolution naturally produces a lockfile without them. * - `installSome`: parse selectors and merge them into the manifest. The - * agent server then resolves the merged manifest, and we read the resolved + * pnpr server then resolves the merged manifest, and we read the resolved * specifiers (with the right save-prefix applied server-side) back from * the lockfile importer entries to update the client-side manifest. * * Returns null if the projects don't map cleanly to allProjects (caller * should fall through to the normal flow). */ -async function prepareAgentProjects ( +async function preparePnprProjects ( projects: MutatedProject[], opts: MutateModulesOptions -): Promise { +): Promise { const allProjects = opts.allProjects ?? [] const mutationByRootDir = new Map() for (const p of projects) { mutationByRootDir.set(p.rootDir, p) } // Include every workspace project, not just the mutated ones — otherwise - // the agent's resulting lockfile would only contain the targeted importer + // the pnpr server's resulting lockfile would only contain the targeted importer // and `headlessInstall` (or a later install) would crash on the missing // entries for the other workspace projects. Projects without a mutation // are sent with their current manifest (no-op for resolution). @@ -2159,7 +2159,7 @@ async function prepareAgentProjects ( } return Promise.all(targetSet.map(async (t) => { let manifest: ProjectManifest = clone(t.manifest) - const newDeps: AgentNewDep[] = [] + const newDeps: PnprNewDep[] = [] const mutation = t.mutation let pinnedVersion: PinnedVersion | undefined if (mutation?.mutation === 'uninstallSome') { @@ -2192,7 +2192,7 @@ async function prepareAgentProjects ( * dependency field per the mutation's `targetDependenciesField` (or the * existing field if the dep is already in the manifest, defaulting to * `dependencies`). Selectors without a version use `'latest'` so the - * agent's resolver picks the newest matching release. + * pnpr server's resolver picks the newest matching release. */ function mergeInstallSelectors (manifest: ProjectManifest, mutation: InstallSomeDepsMutation): ProjectManifest { const target = mutation.targetDependenciesField @@ -2234,16 +2234,16 @@ function findExistingSpec (alias: string, manifest: ProjectManifest): string | u } /** - * After the agent resolves, copy the lockfile importer's per-dep specifier + * After the pnpr server resolves, copy the lockfile importer's per-dep specifier * (which the server's resolver computed with the right save-prefix) back * into the client manifest for any newly added aliases. We rely on the - * lockfile because the agent server applies catalog substitution, + * lockfile because the pnpr server applies catalog substitution, * normalizedBareSpecifier, and save-prefix logic during resolution. */ function applyResolvedSpecsFromLockfile ( manifest: ProjectManifest, importerSnapshot: ProjectSnapshot | undefined, - newDeps: AgentNewDep[], + newDeps: PnprNewDep[], pinnedVersion?: PinnedVersion ): ProjectManifest { if (!importerSnapshot || newDeps.length === 0) return manifest @@ -2258,7 +2258,7 @@ function applyResolvedSpecsFromLockfile ( for (const field of ['dependencies', 'devDependencies', 'optionalDependencies'] as const) { const resolvedVersion = importerSnapshot[field]?.[dep.alias] if (!resolvedVersion || manifest[field]?.[dep.alias] == null) continue - // The agent server resolved the tree but, on the plain-install path, it + // The pnpr server resolved the tree but, on the plain-install path, it // writes the user's raw spec (`'latest'`) into the lockfile specifier // rather than normalizing to a save-prefix range. Compute the // save-prefix spec client-side from the resolved version. @@ -2270,25 +2270,25 @@ function applyResolvedSpecsFromLockfile ( } /** - * Drives the agent path for a `mutateModules` call across one or more - * projects. Returns null if the call can't be served by the agent (e.g. one + * Drives the pnpr server path for a `mutateModules` call across one or more + * projects. Returns null if the call can't be served by the pnpr server (e.g. one * of the projects isn't in `allProjects`). */ -async function mutateModulesViaAgent ( +async function mutateModulesViaPnpr ( projects: MutatedProject[], opts: MutateModulesOptions ): Promise { - const agentProjects = await prepareAgentProjects(projects, opts) - if (!agentProjects) return null + const pnprProjects = await preparePnprProjects(projects, opts) + if (!pnprProjects) return null // installFromPnpmRegistry runs the headless install for the first // project's root and the workspace path for the rest. Pass the // pre-processed manifests so resolution sees the post-mutation state. const result = await installFromPnpmRegistry( - agentProjects[0].manifest, - agentProjects[0].rootDir, + pnprProjects[0].manifest, + pnprProjects[0].rootDir, opts, - agentProjects.map((p) => ({ rootDir: p.rootDir, manifest: p.manifest })) + pnprProjects.map((p) => ({ rootDir: p.rootDir, manifest: p.manifest })) ) // For installSome projects, copy resolved specs from the lockfile importer @@ -2296,7 +2296,7 @@ async function mutateModulesViaAgent ( // effect (the server applies these during its resolution step). const lockfileDir = opts.lockfileDir ?? projects[0].rootDir const mutatedRootDirs = new Set(projects.map((p) => p.rootDir)) - const updatedProjects = agentProjects + const updatedProjects = pnprProjects .filter((p) => mutatedRootDirs.has(p.rootDir)) .map((p) => { if (p.mutation === 'installSome' && p.newDeps.length > 0) { @@ -2317,7 +2317,7 @@ async function mutateModulesViaAgent ( } /** - * When a pnpm agent is configured, resolve dependencies server-side + * When a pnpr server is configured, resolve dependencies server-side * and download only the missing files. Then run a headless install to link * packages into node_modules. */ @@ -2327,22 +2327,22 @@ async function installFromPnpmRegistry ( opts: Opts, allInstallProjects?: Array<{ rootDir: ProjectRootDir, manifest: ProjectManifest }> ): Promise { - // The agent path skips client-side resolution, so resolver-side policies + // The pnpr server path skips client-side resolution, so resolver-side policies // can't be enforced locally. `minimumReleaseAge` is forwarded to the - // agent and enforced server-side. `trustPolicy` has no server-side + // pnpr server and enforced server-side. `trustPolicy` has no server-side // counterpart yet, so refuse to run under it instead of silently // letting through a lockfile the local verifier would reject. if (opts.trustPolicy === 'no-downgrade') { 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.' } + 'TRUST_POLICY_INCOMPATIBLE_WITH_PNPR', + 'The pnpr server does not yet enforce `trustPolicy: no-downgrade`, so running an install through it under this policy would produce a lockfile that the local verifier rejects.', + { hint: 'Unset `trustPolicy` for this install, or disable the pnpr server (unset `--pnpr-server` / `pnprServer` in pnpm-workspace.yaml) so resolution runs locally and the trust check applies.' } ) } - const { fetchFromPnpmRegistry } = await import('@pnpm/agent.client') + const { fetchFromPnpmRegistry } = await import('@pnpm/pnpr.client') const { StoreIndex } = await import('@pnpm/store.index') const { setImportConcurrency } = await import('@pnpm/worker') - // Raise import concurrency for this install only — the agent path has no + // Raise import concurrency for this install only — the pnpr server path has no // concurrent fetching competing for workers. Restore afterwards so we // don't leak a process-wide mutation to other installs (e.g. tests). const restoreImportConcurrency = setImportConcurrency(6) @@ -2351,38 +2351,40 @@ async function installFromPnpmRegistry ( const lockfileDir = opts.lockfileDir ?? rootDir // Read the existing lockfile (if any) in its on-disk shape — that's - // what the agent protocol carries, so no conversion is needed before + // what the pnpr server protocol carries, so no conversion is needed before // sending it. const existingLockfile = await readWantedLockfileFile(lockfileDir, { ignoreIncompatible: true, }).catch(() => null) - logger.info({ message: 'Resolving dependencies via pnpm agent', prefix: rootDir }) + logger.info({ message: 'Resolving dependencies via the pnpr server', prefix: rootDir }) // Open the store index to read integrities and write new entries. // Close it in a finally so a failure in fetchFromPnpmRegistry doesn't // leak an open SQLite handle (on Windows that also blocks store cleanup). const storeIndex = new StoreIndex(opts.storeDir) - let lockfile, agentStats, fileDownloads, indexEntries + let lockfile, pnprStats, fileDownloads, indexEntries try { // Build projects list for workspace support. // Normalize separators to POSIX — on Windows `path.relative` returns - // backslashes, which the agent server rejects (it treats `\` as an + // backslashes, which the pnpr server rejects (it treats `\` as an // unsafe/YAML-injection character and normalizes paths as POSIX). const projectsList = allInstallProjects && allInstallProjects.length > 1 ? allInstallProjects.map(p => ({ dir: (path.relative(lockfileDir, p.rootDir) || '.').split(path.sep).join('/'), dependencies: p.manifest.dependencies, devDependencies: p.manifest.devDependencies, + optionalDependencies: p.manifest.optionalDependencies, })) : undefined - ;({ lockfile, stats: agentStats, fileDownloads, indexEntries } = await fetchFromPnpmRegistry({ - registryUrl: opts.agent!, + ;({ lockfile, stats: pnprStats, fileDownloads, indexEntries } = await fetchFromPnpmRegistry({ + registryUrl: opts.pnprServer!, storeDir: opts.storeDir, storeIndex, dependencies: projectsList ? undefined : manifest.dependencies, devDependencies: projectsList ? undefined : manifest.devDependencies, + optionalDependencies: projectsList ? undefined : manifest.optionalDependencies, projects: projectsList, overrides: opts.overrides, minimumReleaseAge: opts.minimumReleaseAge, @@ -2391,7 +2393,7 @@ async function installFromPnpmRegistry ( })) // Write store index entries so headless install finds them. - const { writeRawIndexEntries } = await import('@pnpm/agent.client') + const { writeRawIndexEntries } = await import('@pnpm/pnpr.client') writeRawIndexEntries(indexEntries, storeIndex) storeIndex.checkpoint() @@ -2409,11 +2411,11 @@ async function installFromPnpmRegistry ( }) logger.info({ - message: `Resolved ${agentStats.totalPackages} packages: ${agentStats.alreadyInStore} cached, ${agentStats.filesToDownload} files to download`, + message: `Resolved ${pnprStats.totalPackages} packages: ${pnprStats.alreadyInStore} cached, ${pnprStats.filesToDownload} files to download`, prefix: rootDir, }) - // `--lockfile-only`: the agent resolved and we wrote the lockfile, but + // `--lockfile-only`: the pnpr server resolved and we wrote the lockfile, but // pnpm fetches nothing and links nothing in this mode — stop before the // headless install. See https://github.com/pnpm/pnpm/issues/12146. if (opts.lockfileOnly) { @@ -2432,8 +2434,8 @@ async function installFromPnpmRegistry ( } // Wrap fetchPackage to: - // 1. Wait for agent file downloads before checking the store - // 2. Skip integrity verification — files just written from the agent + // 1. Wait for pnpr server file downloads before checking the store + // 2. Skip integrity verification — files just written from the pnpr server // are guaranteed correct (server verified, no rehashing needed) const { readPkgFromCafs } = await import('@pnpm/worker') const { storeIndexKey: _storeIndexKey } = await import('@pnpm/store.index') @@ -2445,7 +2447,7 @@ async function installFromPnpmRegistry ( const integrity = resolution?.integrity // Fall through to the regular store controller for git-hosted tarballs. // Their cached entry lives under gitHostedStoreIndexKey (preserves the - // built/not-built dimension), not the integrity-keyed path the agent + // built/not-built dimension), not the integrity-keyed path the pnpr server // uses for npm tarballs. See @pnpm/store.pkg-finder for the rationale. if (integrity && !resolution?.gitHosted) { const filesIndexFile = _storeIndexKey(integrity, fetchOpts.pkg.id) @@ -2469,7 +2471,7 @@ async function installFromPnpmRegistry ( const headlessOpts = { ...opts, - // Skip re-verifying files just written from the agent — they're + // Skip re-verifying files just written from the pnpr server — they're // guaranteed correct (server verified, no rehashing needed). verifyStoreIntegrity: false, storeController: wrappedStoreController, @@ -2516,13 +2518,13 @@ async function installFromPnpmRegistry ( updatedManifest: manifest, ignoredBuilds, // Pacquet doesn't surface a structured stats return; default to - // zeros so the agent-path's non-optional `stats` slot is filled. + // zeros so the pnpr server's non-optional `stats` slot is filled. // The reporter still renders accurate counts from pacquet's // `pnpm:stats` log events. stats: stats ?? { added: 0, removed: 0, linkedToRoot: 0 }, lockfile, - // Server-side resolution (pnpm agent) enforces `minimumReleaseAge` - // itself — the agent picks only mature versions and the lockfile + // Server-side resolution (pnpr server) enforces `minimumReleaseAge` + // itself — the pnpr server picks only mature versions and the lockfile // can't contain immature entries to auto-collect. `trustPolicy` is // guarded above (we refuse to enter this path when it's set), so // there's nothing for the install command to react to here. diff --git a/installing/deps-installer/tsconfig.json b/installing/deps-installer/tsconfig.json index 754a58fc00..45fbead776 100644 --- a/installing/deps-installer/tsconfig.json +++ b/installing/deps-installer/tsconfig.json @@ -24,9 +24,6 @@ { "path": "../../__utils__/test-ipc-server" }, - { - "path": "../../agent/client" - }, { "path": "../../bins/linker" }, @@ -147,6 +144,9 @@ { "path": "../../pkg-manifest/utils" }, + { + "path": "../../pnpr/client" + }, { "path": "../../resolving/parse-wanted-dependency" }, diff --git a/pacquet/crates/pnpr-client/src/lib.rs b/pacquet/crates/pnpr-client/src/lib.rs index f66b3344fc..1c6025461e 100644 --- a/pacquet/crates/pnpr-client/src/lib.rs +++ b/pacquet/crates/pnpr-client/src/lib.rs @@ -1,6 +1,6 @@ //! Client for pnpr's server-accelerated installs. //! -//! Port of the TypeScript `@pnpm/agent.client` (`fetchFromPnpmRegistry`) +//! Port of the TypeScript `@pnpm/pnpr.client` (`fetchFromPnpmRegistry`) //! plus the `fetch-and-write-cafs` worker. Given a set of dependencies //! and the client's content-addressable store, it: //! diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bea2375764..55f2eaacbd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1510,34 +1510,6 @@ importers: specifier: workspace:* version: 'link:' - agent/client: - dependencies: - '@pnpm/lockfile.fs': - specifier: workspace:* - version: link:../../lockfile/fs - '@pnpm/lockfile.types': - specifier: workspace:* - version: link:../../lockfile/types - '@pnpm/logger': - specifier: 'catalog:' - version: 1001.0.1 - '@pnpm/store.cafs': - specifier: workspace:* - version: link:../../store/cafs - '@pnpm/store.index': - specifier: workspace:* - version: link:../../store/index - '@pnpm/worker': - specifier: workspace:^ - version: link:../../worker - devDependencies: - '@pnpm/agent.client': - specifier: workspace:* - version: 'link:' - '@pnpm/types': - specifier: workspace:* - version: link:../../core/types - auth/commands: dependencies: '@inquirer/prompts': @@ -5578,9 +5550,6 @@ importers: '@inquirer/prompts': specifier: 'catalog:' version: 8.5.2(@types/node@25.9.1) - '@pnpm/agent.client': - specifier: workspace:* - version: link:../../agent/client '@pnpm/bins.linker': specifier: workspace:* version: link:../../bins/linker @@ -5713,6 +5682,9 @@ importers: '@pnpm/pkg-manifest.utils': specifier: workspace:* version: link:../../pkg-manifest/utils + '@pnpm/pnpr.client': + specifier: workspace:* + version: link:../../pnpr/client '@pnpm/resolving.parse-wanted-dependency': specifier: workspace:* version: link:../../resolving/parse-wanted-dependency @@ -8084,6 +8056,34 @@ importers: specifier: workspace:* version: 'link:' + pnpr/client: + dependencies: + '@pnpm/lockfile.fs': + specifier: workspace:* + version: link:../../lockfile/fs + '@pnpm/lockfile.types': + specifier: workspace:* + version: link:../../lockfile/types + '@pnpm/logger': + specifier: 'catalog:' + version: 1001.0.1 + '@pnpm/store.cafs': + specifier: workspace:* + version: link:../../store/cafs + '@pnpm/store.index': + specifier: workspace:* + version: link:../../store/index + '@pnpm/worker': + specifier: workspace:^ + version: link:../../worker + devDependencies: + '@pnpm/pnpr.client': + specifier: workspace:* + version: 'link:' + '@pnpm/types': + specifier: workspace:* + version: link:../../core/types + registry-access/client: dependencies: '@pnpm/error': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b352c21a04..2e6a6a063e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -39,7 +39,7 @@ packages: - testing/* - worker - pnpm/artifacts/* - - agent/* + - pnpr/client - registry-access/* - releasing/* - resolving/* diff --git a/pnpm/test/install/pnpmRegistry.ts b/pnpm/test/install/pnpmRegistry.ts index ae1a7edcf1..c45b86d3d4 100644 --- a/pnpm/test/install/pnpmRegistry.ts +++ b/pnpm/test/install/pnpmRegistry.ts @@ -12,7 +12,7 @@ import { execPnpm } from '../utils/index.js' // The pnpr server started by the test harness (see the with-registry jest // preset) serves the install-accelerator endpoints (/v1/install, /v1/files) -// on the registry-mock port, so it doubles as the agent under test. +// on the registry-mock port, so it doubles as the pnpr server under test. const PNPR = `http://localhost:${REGISTRY_MOCK_PORT}` let server: http.Server @@ -21,7 +21,7 @@ let requestCount: number beforeAll(async () => { // Counting proxy — forwards to the pnpr server and counts /v1/install - // requests so we can assert that the agent path was actually taken. + // requests so we can assert that the pnpr server path was actually taken. requestCount = 0 server = http.createServer((req, res) => { if (!req.url) { @@ -55,7 +55,7 @@ afterAll(async () => { await new Promise((resolve) => server.close(() => resolve())) }) -test('pnpm install uses pnpm agent when configured', async () => { +test('pnpm install uses pnpr server when configured', async () => { prepare({ dependencies: { 'is-positive': '1.0.0', @@ -65,7 +65,7 @@ test('pnpm install uses pnpm agent when configured', async () => { requestCount = 0 await execPnpm( - ['install', `--config.agent=http://localhost:${serverPort}`] + ['install', `--config.pnprServer=http://localhost:${serverPort}`] ) // Verify the registry server received at least one request @@ -78,25 +78,48 @@ test('pnpm install uses pnpm agent when configured', async () => { expect(fs.existsSync('node_modules/is-positive')).toBe(true) }) -test('a second resolution forwards the existing lockfile to the agent', async () => { +test('pnpm install resolves optionalDependencies via the pnpr server', async () => { + prepare({ + dependencies: { + 'is-positive': '1.0.0', + }, + optionalDependencies: { + 'is-negative': '1.0.0', + }, + }) + + requestCount = 0 + + await execPnpm( + ['install', `--config.pnprServer=http://localhost:${serverPort}`] + ) + + expect(requestCount).toBeGreaterThanOrEqual(1) + expect(fs.existsSync('node_modules/is-positive')).toBe(true) + // The optional dependency must be forwarded to the server and resolved, + // not silently dropped from the request. + expect(fs.existsSync('node_modules/is-negative')).toBe(true) +}) + +test('a second resolution forwards the existing lockfile to the pnpr server', async () => { prepare({}) // First add creates the lockfile. - await execPnpm(['add', 'is-positive@1.0.0', `--config.agent=http://localhost:${serverPort}`]) + await execPnpm(['add', 'is-positive@1.0.0', `--config.pnprServer=http://localhost:${serverPort}`]) expect(fs.existsSync(WANTED_LOCKFILE)).toBe(true) - // Second add reads that lockfile and forwards it to the agent for + // Second add reads that lockfile and forwards it to the pnpr server for // incremental resolution — exercises the on-disk lockfile being sent // over the wire without an in-memory round-trip. requestCount = 0 - await execPnpm(['add', 'is-negative@1.0.0', `--config.agent=http://localhost:${serverPort}`]) + await execPnpm(['add', 'is-negative@1.0.0', `--config.pnprServer=http://localhost:${serverPort}`]) expect(requestCount).toBeGreaterThanOrEqual(1) expect(fs.existsSync('node_modules/is-positive')).toBe(true) expect(fs.existsSync('node_modules/is-negative')).toBe(true) }) -test('pnpm add uses pnpm agent when configured', async () => { +test('pnpm add uses pnpr server when configured', async () => { prepare({ dependencies: { 'is-negative': '1.0.0', @@ -106,7 +129,7 @@ test('pnpm add uses pnpm agent when configured', async () => { requestCount = 0 await execPnpm( - ['add', 'is-positive@1.0.0', `--config.agent=http://localhost:${serverPort}`] + ['add', 'is-positive@1.0.0', `--config.pnprServer=http://localhost:${serverPort}`] ) expect(requestCount).toBeGreaterThanOrEqual(1) @@ -122,7 +145,7 @@ test('pnpm add uses pnpm agent when configured', async () => { expect(manifest.dependencies?.['is-negative']).toBe('1.0.0') }) -test('pnpm remove uses pnpm agent when configured', async () => { +test('pnpm remove uses pnpr server when configured', async () => { prepare({ dependencies: { 'is-positive': '1.0.0', @@ -133,7 +156,7 @@ test('pnpm remove uses pnpm agent when configured', async () => { requestCount = 0 await execPnpm( - ['remove', 'is-negative', `--config.agent=http://localhost:${serverPort}`] + ['remove', 'is-negative', `--config.pnprServer=http://localhost:${serverPort}`] ) expect(requestCount).toBeGreaterThanOrEqual(1) @@ -147,32 +170,32 @@ test('pnpm remove uses pnpm agent when configured', async () => { expect(manifest.dependencies?.['is-negative']).toBeUndefined() }) -test('pnpm add without a version uses the pnpm agent and writes the save-prefix spec from the lockfile', async () => { +test('pnpm add without a version uses the pnpr server and writes the save-prefix spec from the lockfile', async () => { prepare({}) requestCount = 0 await execPnpm( - ['add', 'is-positive', `--config.agent=http://localhost:${serverPort}`] + ['add', 'is-positive', `--config.pnprServer=http://localhost:${serverPort}`] ) expect(requestCount).toBeGreaterThanOrEqual(1) expect(fs.existsSync('node_modules/is-positive')).toBe(true) const manifest = loadJsonFileSync<{ dependencies?: Record }>('package.json') - // The agent resolves "latest" to a concrete version and writes the + // The pnpr server resolves "latest" to a concrete version and writes the // resolved version into the lockfile importer's `dependencies` map; the // client computes the save-prefix spec from that version. expect(manifest.dependencies?.['is-positive']).toMatch(/^\^\d+\.\d+\.\d+$/) }) -test('pnpm add -D uses pnpm agent and targets devDependencies', async () => { +test('pnpm add -D uses pnpr server and targets devDependencies', async () => { prepare({}) requestCount = 0 await execPnpm( - ['add', '-D', 'is-positive@1.0.0', `--config.agent=http://localhost:${serverPort}`] + ['add', '-D', 'is-positive@1.0.0', `--config.pnprServer=http://localhost:${serverPort}`] ) expect(requestCount).toBeGreaterThanOrEqual(1) @@ -186,13 +209,13 @@ test('pnpm add -D uses pnpm agent and targets devDependencies', async () => { expect(manifest.dependencies?.['is-positive']).toBeUndefined() }) -test('pnpm add with multiple selectors uses pnpm agent', async () => { +test('pnpm add with multiple selectors uses pnpr server', async () => { prepare({}) requestCount = 0 await execPnpm( - ['add', 'is-positive@1.0.0', 'is-negative@1.0.0', `--config.agent=http://localhost:${serverPort}`] + ['add', 'is-positive@1.0.0', 'is-negative@1.0.0', `--config.pnprServer=http://localhost:${serverPort}`] ) expect(requestCount).toBeGreaterThanOrEqual(1) @@ -204,7 +227,7 @@ test('pnpm add with multiple selectors uses pnpm agent', async () => { expect(manifest.dependencies?.['is-negative']).toBe('1.0.0') }) -test('pnpm --filter remove inside a workspace uses pnpm agent', async () => { +test('pnpm --filter remove inside a workspace uses pnpr server', async () => { preparePackages([ { name: 'project-a', @@ -228,7 +251,7 @@ test('pnpm --filter remove inside a workspace uses pnpm agent', async () => { requestCount = 0 await execPnpm( - ['--filter=project-b', 'remove', 'is-negative', `--config.agent=http://localhost:${serverPort}`] + ['--filter=project-b', 'remove', 'is-negative', `--config.pnprServer=http://localhost:${serverPort}`] ) expect(requestCount).toBeGreaterThanOrEqual(1) @@ -244,7 +267,7 @@ test('pnpm --filter remove inside a workspace uses pnpm agent', async () => { expect(projectBManifest.dependencies?.['is-positive']).toBe('1.0.0') }) -test('pnpm add inside a workspace project uses pnpm agent', async () => { +test('pnpm add inside a workspace project uses pnpr server', async () => { preparePackages([ { name: 'project-a', @@ -264,7 +287,7 @@ test('pnpm add inside a workspace project uses pnpm agent', async () => { requestCount = 0 await execPnpm( - ['--filter=project-b', 'add', 'is-negative@1.0.0', `--config.agent=http://localhost:${serverPort}`] + ['--filter=project-b', 'add', 'is-negative@1.0.0', `--config.pnprServer=http://localhost:${serverPort}`] ) expect(requestCount).toBeGreaterThanOrEqual(1) @@ -279,7 +302,7 @@ test('pnpm add inside a workspace project uses pnpm agent', async () => { expect(projectBManifest.dependencies?.['is-negative']).toBe('1.0.0') }) -test('pnpm install with agent works in a workspace with multiple projects', async () => { +test('pnpm install with pnpr server works in a workspace with multiple projects', async () => { preparePackages([ { name: 'project-a', @@ -302,10 +325,10 @@ test('pnpm install with agent works in a workspace with multiple projects', asyn requestCount = 0 await execPnpm( - ['install', `--config.agent=http://localhost:${serverPort}`] + ['install', `--config.pnprServer=http://localhost:${serverPort}`] ) - // Verify the agent server was used + // Verify the pnpr server was used expect(requestCount).toBeGreaterThanOrEqual(1) // Verify the lockfile was created diff --git a/agent/client/.gitignore b/pnpr/client/.gitignore similarity index 100% rename from agent/client/.gitignore rename to pnpr/client/.gitignore diff --git a/agent/client/CHANGELOG.md b/pnpr/client/CHANGELOG.md similarity index 100% rename from agent/client/CHANGELOG.md rename to pnpr/client/CHANGELOG.md diff --git a/agent/client/README.md b/pnpr/client/README.md similarity index 70% rename from agent/client/README.md rename to pnpr/client/README.md index 8aa557dd4f..ef75664188 100644 --- a/agent/client/README.md +++ b/pnpr/client/README.md @@ -1,11 +1,11 @@ -# @pnpm/agent.client +# @pnpm/pnpr.client -Client library for the pnpm agent server. Reads the local store state, sends it to the server, and writes the received files into the content-addressable store. +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. ## How it works 1. Reads integrity hashes from the local store index (`index.db`). -2. Sends `POST /v1/install` to the pnpm agent server with the project's dependencies and the store integrities. +2. Sends `POST /v1/install` to the pnpr server with the project's dependencies and the store integrities. 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. 4. File download workers write each received file directly to the local CAFS (`files/{hash[:2]}/{hash[2:]}`). 5. Writes store index entries for all new packages in a single SQLite transaction. @@ -13,10 +13,10 @@ Client library for the pnpm agent server. Reads the local store state, sends it ## Usage -This package is used internally by pnpm when the `agent` config option is set. It is not intended to be called directly, but can be used programmatically: +This package is used internally by pnpm when the `pnprServer` config option is set. It is not intended to be called directly, but can be used programmatically: ```typescript -import { fetchFromPnpmRegistry } from '@pnpm/agent.client' +import { fetchFromPnpmRegistry } from '@pnpm/pnpr.client' import { StoreIndex } from '@pnpm/store.index' const storeIndex = new StoreIndex('/path/to/store') @@ -39,5 +39,5 @@ console.log(`${stats.alreadyInStore} cached, ${stats.filesToDownload} files down Add to `pnpm-workspace.yaml` to enable automatically during `pnpm install`: ```yaml -agent: http://localhost:4000 +pnprServer: http://localhost:4000 ``` diff --git a/agent/client/package.json b/pnpr/client/package.json similarity index 74% rename from agent/client/package.json rename to pnpr/client/package.json index d73ed1f9e2..b9ace94c2d 100644 --- a/agent/client/package.json +++ b/pnpr/client/package.json @@ -1,15 +1,15 @@ { - "name": "@pnpm/agent.client", + "name": "@pnpm/pnpr.client", "version": "1.0.8", - "description": "Client for pnpm agent server — sends store state, receives resolved lockfile and missing files", + "description": "Client for the pnpr server — sends store state, receives resolved lockfile and missing files", "keywords": [ "pnpm", "pnpm11" ], "license": "MIT", "funding": "https://opencollective.com/pnpm", - "repository": "https://github.com/pnpm/pnpm/tree/main/agent/client", - "homepage": "https://github.com/pnpm/pnpm/tree/main/agent/client#readme", + "repository": "https://github.com/pnpm/pnpm/tree/main/pnpr/client", + "homepage": "https://github.com/pnpm/pnpm/tree/main/pnpr/client#readme", "bugs": { "url": "https://github.com/pnpm/pnpm/issues" }, @@ -41,7 +41,7 @@ "@pnpm/worker": "workspace:^" }, "devDependencies": { - "@pnpm/agent.client": "workspace:*", + "@pnpm/pnpr.client": "workspace:*", "@pnpm/types": "workspace:*" }, "engines": { diff --git a/agent/client/src/fetchFromPnpmRegistry.ts b/pnpr/client/src/fetchFromPnpmRegistry.ts similarity index 91% rename from agent/client/src/fetchFromPnpmRegistry.ts rename to pnpr/client/src/fetchFromPnpmRegistry.ts index a2018a94eb..bf8b34bf7a 100644 --- a/agent/client/src/fetchFromPnpmRegistry.ts +++ b/pnpr/client/src/fetchFromPnpmRegistry.ts @@ -9,15 +9,16 @@ import { fetchAndWriteCafsFiles } from '@pnpm/worker' import type { ResponseMetadata } from './protocol.js' -export interface AgentProject { +export interface PnprProject { /** Relative dir within the workspace (e.g. "." or "packages/foo") */ dir: string dependencies?: Record devDependencies?: Record + optionalDependencies?: Record } export interface FetchFromPnpmRegistryOptions { - /** URL of the pnpm agent server */ + /** URL of the pnpr server */ registryUrl: string /** Client's store directory */ storeDir: string @@ -27,8 +28,10 @@ export interface FetchFromPnpmRegistryOptions { dependencies?: Record /** Dev dependencies to resolve (single project) */ devDependencies?: Record + /** Optional dependencies to resolve (single project) */ + optionalDependencies?: Record /** Multiple projects in a workspace */ - projects?: AgentProject[] + projects?: PnprProject[] /** Overrides */ overrides?: Record /** Node.js version for resolution */ @@ -62,7 +65,7 @@ export interface FetchFromPnpmRegistryResult { } /** - * Fetch resolved dependencies from a pnpm agent server. + * Fetch resolved dependencies from a pnpr server. * * The response is a streaming NDJSON where each line is one message: * - `D\t{digest}\t{size}\t{executable}\n` — file digest (streamed as packages resolve) @@ -81,6 +84,7 @@ export async function fetchFromPnpmRegistry ( dir: '.', dependencies: opts.dependencies, devDependencies: opts.devDependencies, + optionalDependencies: opts.optionalDependencies, }] const requestBody = JSON.stringify({ @@ -169,7 +173,7 @@ export async function fetchFromPnpmRegistry ( } else if (type === 'E') { // Server emitted a structured error after headers were sent. // Record it so stream `end` / `catch` can reject with the payload. - let message = 'pnpm agent server error' + let message = 'pnpr server error' try { const payload = JSON.parse(line.substring(tabIdx + 1)) as { error?: string } if (payload?.error) message = payload.error @@ -189,7 +193,7 @@ export async function fetchFromPnpmRegistry ( if (serverError) { reject(serverError) } else if (!resolved) { - reject(new Error('pnpm agent server closed the stream without emitting a lockfile')) + reject(new Error('pnpr server closed the stream without emitting a lockfile')) } }, reject) }) @@ -203,7 +207,7 @@ function readStoreIntegrities (storeIndex: StoreIndex): string[] { const integrity = key.slice(0, tabIdx) // StoreIndex also stores non-integrity keys (e.g. git-hosted entries // keyed by URL). Filter to actual SRI hashes — sending those over to - // the agent server would just bloat the request without ever matching. + // the pnpr server would just bloat the request without ever matching. if (!isIntegrityLike(integrity)) continue seen.add(integrity) } @@ -231,7 +235,7 @@ async function streamNdjsonRequest ( ): Promise { // `urlPath` is expected to be relative (e.g. "v1/install"). We normalize // the base to end with "/" so `new URL(rel, base)` preserves any path - // prefix configured on the agent URL (e.g. https://host/pnpr/). + // prefix configured on the pnpr server URL (e.g. https://host/pnpr/). const base = registryUrl.endsWith('/') ? registryUrl : `${registryUrl}/` const url = new URL(urlPath, base) const isHttps = url.protocol === 'https:' @@ -251,7 +255,7 @@ async function streamNdjsonRequest ( const chunks: Buffer[] = [] res.on('data', (chunk: Buffer) => chunks.push(chunk)) res.on('end', () => { - reject(new Error(`pnpm agent responded with ${res.statusCode}: ${Buffer.concat(chunks).toString('utf-8')}`)) + reject(new Error(`pnpr server responded with ${res.statusCode}: ${Buffer.concat(chunks).toString('utf-8')}`)) }) return } @@ -274,11 +278,11 @@ async function streamNdjsonRequest ( }) req.on('timeout', () => { - req.destroy(new Error(`pnpm agent request timed out after ${REQUEST_TIMEOUT / 1000}s (${registryUrl})`)) + req.destroy(new Error(`pnpr server request timed out after ${REQUEST_TIMEOUT / 1000}s (${registryUrl})`)) }) req.on('error', (err: NodeJS.ErrnoException) => { if (err.code === 'ECONNREFUSED') { - reject(new Error(`Could not connect to pnpm agent at ${registryUrl}. Is the server running?`)) + reject(new Error(`Could not connect to pnpr server at ${registryUrl}. Is the server running?`)) } else { reject(err) } diff --git a/agent/client/src/index.ts b/pnpr/client/src/index.ts similarity index 100% rename from agent/client/src/index.ts rename to pnpr/client/src/index.ts diff --git a/agent/client/src/protocol.ts b/pnpr/client/src/protocol.ts similarity index 100% rename from agent/client/src/protocol.ts rename to pnpr/client/src/protocol.ts diff --git a/agent/client/tsconfig.json b/pnpr/client/tsconfig.json similarity index 100% rename from agent/client/tsconfig.json rename to pnpr/client/tsconfig.json diff --git a/agent/client/tsconfig.lint.json b/pnpr/client/tsconfig.lint.json similarity index 100% rename from agent/client/tsconfig.lint.json rename to pnpr/client/tsconfig.lint.json diff --git a/pnpr/crates/pnpr/src/install_accelerator/protocol.rs b/pnpr/crates/pnpr/src/install_accelerator/protocol.rs index 72f1344b2c..4f04f395ca 100644 --- a/pnpr/crates/pnpr/src/install_accelerator/protocol.rs +++ b/pnpr/crates/pnpr/src/install_accelerator/protocol.rs @@ -1,5 +1,5 @@ //! Wire types for the pnpr install-accelerator endpoints, matching the -//! `@pnpm/agent.client` TypeScript client's request shapes. +//! `@pnpm/pnpr.client` TypeScript client's request shapes. use std::collections::BTreeMap; @@ -18,6 +18,8 @@ pub struct InstallRequestProject { pub dependencies: DepMap, #[serde(default)] pub dev_dependencies: DepMap, + #[serde(default)] + pub optional_dependencies: DepMap, } fn root_dir() -> String { @@ -39,6 +41,8 @@ pub struct InstallRequest { #[serde(default)] pub dev_dependencies: Option, #[serde(default)] + pub optional_dependencies: Option, + #[serde(default)] pub projects: Option>, #[serde(default)] pub store_integrities: Vec, @@ -119,6 +123,7 @@ pub struct ProjectDeps { pub dir: String, pub dependencies: DepMap, pub dev_dependencies: DepMap, + pub optional_dependencies: DepMap, } impl InstallRequest { @@ -134,6 +139,7 @@ impl InstallRequest { dir: project.dir.clone(), dependencies: project.dependencies.clone(), dev_dependencies: project.dev_dependencies.clone(), + optional_dependencies: project.optional_dependencies.clone(), }) .collect(); } @@ -141,6 +147,7 @@ impl InstallRequest { dir: root_dir(), dependencies: self.dependencies.clone().unwrap_or_default(), dev_dependencies: self.dev_dependencies.clone().unwrap_or_default(), + optional_dependencies: self.optional_dependencies.clone().unwrap_or_default(), }] } } diff --git a/pnpr/crates/pnpr/src/install_accelerator/resolve.rs b/pnpr/crates/pnpr/src/install_accelerator/resolve.rs index e6f01641a6..366e957f08 100644 --- a/pnpr/crates/pnpr/src/install_accelerator/resolve.rs +++ b/pnpr/crates/pnpr/src/install_accelerator/resolve.rs @@ -101,6 +101,7 @@ pub async fn resolve( "version": "0.0.0", "dependencies": project.dependencies, "devDependencies": project.dev_dependencies, + "optionalDependencies": project.optional_dependencies, }); let manifest_bytes = serde_json::to_vec(&manifest_json) .map_err(|err| ResolveError::Install(err.to_string()))?; diff --git a/worker/src/index.ts b/worker/src/index.ts index 1c5de65956..0d50f982d4 100644 --- a/worker/src/index.ts +++ b/worker/src/index.ts @@ -284,7 +284,7 @@ export async function readPkgFromCafs ( let limitImportingPackage = pLimit(4) /** - * Temporarily change import concurrency. Called by the pnpm agent code path + * Temporarily change import concurrency. Called by the pnpr server code path * where there's no concurrent fetching competing for workers. Returns a * disposer that restores the previous limiter — callers must invoke it (in a * finally block) to avoid leaking the mutation to other installs in the same diff --git a/worker/src/start.ts b/worker/src/start.ts index b423d9a66d..e093904ca0 100644 --- a/worker/src/start.ts +++ b/worker/src/start.ts @@ -508,7 +508,7 @@ async function fetchAndWriteCafs (message: FetchAndWriteCafsMessage): Promise<{ const { createGunzip } = await import('node:zlib') const { contentPathFromHex } = await import('@pnpm/store.cafs') - // Preserve any path prefix on the agent URL (e.g. https://host/pnpr/) + // Preserve any path prefix on the pnpr server URL (e.g. https://host/pnpr/) // by normalizing the base and using a relative URL. const base = message.registryUrl.endsWith('/') ? message.registryUrl : `${message.registryUrl}/` const url = new URL('v1/files', base) @@ -517,7 +517,7 @@ async function fetchAndWriteCafs (message: FetchAndWriteCafsMessage): Promise<{ const createdDirs = new Set() // Build a set of digests we actually requested, so we can reject a - // misbehaving agent that streams unrelated entries and tries to write + // misbehaving pnpr server that streams unrelated entries and tries to write // unbounded files into our CAFS. The set is keyed by `${digest}:${exec}` // because the same digest may appear with different modes. const requestedDigests = new Set() @@ -565,7 +565,7 @@ async function fetchAndWriteCafs (message: FetchAndWriteCafsMessage): Promise<{ const requestKey = `${digest}:${executable ? 'x' : ''}` if (!requestedDigests.has(requestKey)) { - throw new Error(`pnpm agent /v1/files returned an entry that was not requested: digest=${digest} executable=${String(executable)}`) + throw new Error(`pnpr server /v1/files returned an entry that was not requested: digest=${digest} executable=${String(executable)}`) } // Consume the request so duplicates past the requested count also fail. requestedDigests.delete(requestKey) @@ -588,7 +588,7 @@ async function fetchAndWriteCafs (message: FetchAndWriteCafsMessage): Promise<{ // EEXIST means the same digest is already at this CAFS path. CAFS // is content-addressed, so a complete file is by definition correct. // But a previous process could have crashed mid-write and left a - // truncated file — the agent path skips integrity verification, so + // truncated file — the pnpr server path skips integrity verification, so // we'd silently install garbage. Detect truncation by size and // overwrite atomically if the on-disk file is the wrong length. const onDiskSize = fs.statSync(fullPath).size @@ -630,13 +630,13 @@ async function fetchAndWriteCafs (message: FetchAndWriteCafsMessage): Promise<{ 'Accept-Encoding': 'gzip', }, }, (res: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any - // Non-2xx responses are JSON error bodies from the agent server; read + // Non-2xx responses are JSON error bodies from the pnpr server; read // and reject so we never try to gunzip an error payload as a file stream. if (typeof res.statusCode === 'number' && (res.statusCode < 200 || res.statusCode >= 300)) { const chunks: Buffer[] = [] res.on('data', (chunk: Buffer) => chunks.push(chunk)) res.on('end', () => { - reject(new Error(`pnpm agent /v1/files responded with ${res.statusCode}: ${Buffer.concat(chunks).toString('utf-8')}`)) + reject(new Error(`pnpr server /v1/files responded with ${res.statusCode}: ${Buffer.concat(chunks).toString('utf-8')}`)) }) res.on('error', reject) return @@ -660,11 +660,11 @@ async function fetchAndWriteCafs (message: FetchAndWriteCafsMessage): Promise<{ // partial entry still in `buf`, fail — otherwise we'd silently leave // the CAFS missing files. if (!endMarkerSeen) { - reject(new Error('pnpm agent /v1/files stream ended without the end marker')) + reject(new Error('pnpr server /v1/files stream ended without the end marker')) return } if (buf.length > 0) { - reject(new Error(`pnpm agent /v1/files stream left ${buf.length} unparsed bytes after end marker`)) + reject(new Error(`pnpr server /v1/files stream left ${buf.length} unparsed bytes after end marker`)) return } // Every received entry was drained from `requestedDigests` as it was @@ -672,7 +672,7 @@ async function fetchAndWriteCafs (message: FetchAndWriteCafsMessage): Promise<{ // but omitted files, which would silently leave the CAFS incomplete. if (requestedDigests.size > 0) { const sample = [...requestedDigests].slice(0, 3).join(', ') - reject(new Error(`pnpm agent /v1/files omitted ${requestedDigests.size} requested entries (e.g. ${sample})`)) + reject(new Error(`pnpr server /v1/files omitted ${requestedDigests.size} requested entries (e.g. ${sample})`)) return } resolve({ status: 'success', filesWritten }) @@ -680,7 +680,7 @@ async function fetchAndWriteCafs (message: FetchAndWriteCafsMessage): Promise<{ stream.on('error', reject) }) req.on('timeout', () => { - req.destroy(new Error(`pnpm agent /v1/files request timed out after ${FILES_REQUEST_TIMEOUT_MS / 1000}s`)) + req.destroy(new Error(`pnpr server /v1/files request timed out after ${FILES_REQUEST_TIMEOUT_MS / 1000}s`)) }) req.on('error', reject) req.write(body)