pnpm 12.3.4 → 12.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,208 @@
1
1
  # pnpm
2
2
 
3
+ ## 12.4.1
4
+
5
+ pnpm 12.4.1 fixes installs that failed on filesystems refusing hard links or clones, on Android, and under `nodeLinker: hoisted`. Repeat installs are faster.
6
+
7
+ ### Patch Changes
8
+
9
+ #### Installing packages
10
+
11
+ - `pnpm install` no longer fails with `Operation not permitted` when the filesystem refuses a hard link or a copy-on-write clone [#14722](https://github.com/pnpm/pnpm/issues/14722). Under `packageImportMethod: auto` and `clone-or-copy`, pnpm copies the file instead. EdenFS checkouts, which have no hard links, and rootless containers, which refuse the clone syscall, both hit this. An explicit `packageImportMethod: hardlink` or `clone` still reports the error.
12
+
13
+ pnpm also copies a package file whose store entry has reached the filesystem's limit on names for one file, 1024 on NTFS and 65000 on ext4. Such a file failed the install under `packageImportMethod: hardlink`, and under `auto` it stopped pnpm hard linking for the rest of the install.
14
+
15
+ - `pnpm install` no longer writes a package file through a symlink left at the path it is importing to. Copying such a file overwrote whatever the link pointed at, and created that file when the link pointed nowhere. An executable package file also made the link's target executable.
16
+
17
+ - Fixed `pnpm install` and `pnpm dlx` on Android. Registry requests crashed because pnpm found no system CA certificates, so pnpm uses bundled ones there [#14777](https://github.com/pnpm/pnpm/issues/14777). Imports also failed with "Permission denied" on filesystems that deny hard links and reflinks, and now fall back to copying [#14780](https://github.com/pnpm/pnpm/issues/14780).
18
+
19
+ - `pnpm install` no longer fails with "Invalid cross-device link" while preserving a package's nested `node_modules` directory during a Docker build [#14758](https://github.com/pnpm/pnpm/issues/14758).
20
+
21
+ - `pnpm install` no longer fails on a package tarball that carries a file at the archive root, such as the `._*` entries macOS `tar` adds [#14701](https://github.com/pnpm/pnpm/issues/14701). The file is installed at the root of the package.
22
+
23
+ A `file:` tarball packed without the usual `package/` directory is now recorded under the name and version from its own `package.json`. It was recorded under the alias the dependency was given, at version 0.0.0.
24
+
25
+ - Under `nodeLinker: hoisted`, `pnpm install` no longer re-imports packages that are already in place. A repeat install replaced the whole `node_modules` tree and reported `Packages: +N`. A package is still imported when its directory is missing, when its `package.json` no longer carries the installed version, when it is a `file:` dependency, and when it is patched. Lifecycle scripts no longer run again for a package left in place, and `pnpm rebuild` and a change to `allowBuilds` still reach it.
26
+
27
+ - `pnpm install` now runs a dependency's build scripts again when its side-effects cache entry has no files to restore [#14717](https://github.com/pnpm/pnpm/issues/14717). Such builds were skipped and nothing was put in their place, so a script whose whole effect lands outside its own package directory, such as a git hook installer, never took effect. pnpm no longer publishes empty artifacts to the shared side-effects cache either.
28
+
29
+ #### Resolving and linking dependencies
30
+
31
+ - `pnpm install`, `pnpm add`, and `pnpm dedupe` now apply `ignoredOptionalDependencies` [#14729](https://github.com/pnpm/pnpm/issues/14729). Matching optional dependencies are left out of the lockfile and are not installed. pnpm 12 installed them whenever it resolved dependencies from scratch.
32
+
33
+ - `pnpm install` no longer links a transitive dependency to a workspace package when `linkWorkspacePackages` is `true` and the dependency is declared with a plain version range [#14781](https://github.com/pnpm/pnpm/issues/14781). Enabling `preferWorkspacePackages` does not change this. Set `linkWorkspacePackages: deep` to link them.
34
+
35
+ - `pnpm install` no longer leaves dangling dependency links in workspace packages located above the workspace root [#14726](https://github.com/pnpm/pnpm/issues/14726).
36
+
37
+ - `pnpm install` and `pnpm add` no longer leave a dangling symlink in `node_modules` when a project starts depending directly on a package that the lockfile holds only as a transitive dependency with resolved peer dependencies [#14714](https://github.com/pnpm/pnpm/issues/14714).
38
+
39
+ - `pnpm dedupe` now keeps a compatible auto-installed peer when another workspace project depends on a newer major [#14697](https://github.com/pnpm/pnpm/issues/14697). Repeated runs alternated between compatible and incompatible peer versions.
40
+
41
+ - `pnpm peers check` no longer reports a peer dependency declared as `workspace:^`, `workspace:~`, or a bare `workspace:` as unmet [#14770](https://github.com/pnpm/pnpm/issues/14770). pnpm reported these as unmet whatever version the linked workspace project supplied.
42
+
43
+ #### Performance
44
+
45
+ - Sped up repeat installs [#14540](https://github.com/pnpm/pnpm/issues/14540). pnpm checks the store's files only for the packages it links into `node_modules`, instead of every package in the lockfile. Creating the command shims in `node_modules/.bin` makes about 1,500 fewer filesystem calls in a 76 project workspace. Installs that use the global virtual store read their slot paths from the cache directory instead of deriving them every time. Verifying a large lockfile also allocates less memory.
46
+
47
+ - Sped up `pnpm install` in Cargo workspaces with many member crates. Repeated installs reuse verified Cargo checksum metadata.
48
+
49
+ - Installing several packages from the same Git repository and commit now downloads the source once per install [#14725](https://github.com/pnpm/pnpm/issues/14725). Each package still runs its prepare scripts in its own copy of the checkout.
50
+
51
+ #### Running scripts and tasks
52
+
53
+ - pnpm now passes Ctrl+C on to the script or command it started and waits for it to shut down [#14723](https://github.com/pnpm/pnpm/issues/14723). pnpm exited first, so a script that was still writing landed on the shell prompt.
54
+
55
+ - `pnpm run "/pattern/" --no-bail` now lets every matched script finish after one of them fails [#14718](https://github.com/pnpm/pnpm/issues/14718). The command exits with `ERR_PNPM_RUN_FAILED`, and its message lists the scripts that failed in the order they were selected.
56
+
57
+ - `pnpm pipeline` no longer fails on a project that tracks a symlink, such as a `CLAUDE.md` pointing at `AGENTS.md` [#14692](https://github.com/pnpm/pnpm/issues/14692). Changing a symlinked input's target invalidates that task's cache, and `pnpm pipeline --no-cache` no longer hashes task inputs.
58
+
59
+ #### Commands
60
+
61
+ - `pnpm add -g`, `pnpm update -g`, and `pnpm remove -g` no longer change global bins or install directories after reading only part of an installed package group [#13796](https://github.com/pnpm/pnpm/issues/13796). If any declared package manifest is missing, malformed, or unreadable, pnpm now fails before it activates or removes anything and leaves the existing global installation intact.
62
+
63
+ - `pnpm dedupe` now processes every workspace project by default, including workspaces that keep a separate lockfile per project [#14732](https://github.com/pnpm/pnpm/issues/14732). Workspace filters select which projects it processes, and `--fail-if-no-match` exits with an error when no project matches.
64
+
65
+ - `pnpm update <name>@<version>` now keeps the range operator the manifest declares [#14745](https://github.com/pnpm/pnpm/issues/14745). Running `pnpm update react@19.3.0` on `"react": "^19.2.8"` writes `"react": "^19.3.0"`. A `jsr:` entry keeps its `jsr:` prefix, and a plain `pnpm update` now moves a `jsr:` range the way it moves an npm range.
66
+
67
+ - `pnpm --filter` directory selectors now support `?` wildcards and character classes such as `[ab]`. A `*` or `?` wildcard no longer selects a directory whose name starts with a dot, as on pnpm 11.
68
+
69
+ - `pnpm deploy --legacy` now prefers the dependency versions pinned in the source workspace lockfile when they still satisfy the deployed project's range [#13857](https://github.com/pnpm/pnpm/issues/13857).
70
+
71
+ - `pnpm sbom` now leaves out a package's author field when the manifest author name is empty or contains only whitespace [#14685](https://github.com/pnpm/pnpm/issues/14685). In a filtered or split workspace run, only a project with no `author` field inherits the workspace root's author.
72
+
73
+ `pnpm sbom --sbom-format spdx` now writes `creationInfo.created` with whole seconds, such as `2026-09-08T10:38:21Z` [#14684](https://github.com/pnpm/pnpm/issues/14684). The fractional seconds it carried were rejected by strict SPDX consumers.
74
+
75
+ #### Configuration
76
+
77
+ - The `updateConfig` pnpmfile hook now receives the resolved configuration, including settings that came from `.npmrc`, the command line, or a default [#14676](https://github.com/pnpm/pnpm/issues/14676). Scoped registries are reported under `registriesByScope`, and a hook may rewrite that map to change where packages are fetched from. Registry credentials are reported under `configByUri`, as pnpm 11 reports them. An unset setting is left out rather than reported as `null`.
78
+
79
+ - `pnpm audit --fix` and the `minimumReleaseAgeStrict` approval prompt now keep the comments in `minimumReleaseAgeExclude` when they append an entry to it in `pnpm-workspace.yaml`. The rest of the list is left as written, and the `trustPolicyExcludePrune` and `minimumReleaseAgeExcludePrune` cleanups keep the comments of the entries they retain.
80
+
81
+ `pnpm install` and `pnpm dedupe` now run those cleanups too [#14759](https://github.com/pnpm/pnpm/issues/14759). Only `pnpm add`, `pnpm update`, and `pnpm remove` pruned the entries that the freshly written lockfile no longer resolves.
82
+
83
+ - `pnpm config set --global node-download-mirrors` no longer rejects the key [#13611](https://github.com/pnpm/pnpm/issues/13611). The global config file already accepted `nodeDownloadMirrors`, but the command refused to write it.
84
+
85
+ - `NO_PROXY` entries that start with a dot, such as `.npmjs.org`, now bypass the proxy for the domain and its subdomains [#14686](https://github.com/pnpm/pnpm/issues/14686).
86
+
87
+ - pnpm no longer creates a project `pnpm-lock.yaml` when `devEngines.packageManager.onFail` is `download` and lockfile writing is off through `lockfile: false` or `--no-lockfile` [#14728](https://github.com/pnpm/pnpm/issues/14728). pnpm still switches to the pinned version.
88
+
89
+ - pnpm now writes `node_modules/.package-map.json` only when `nodeExperimentalPackageMap` is enabled. Nothing reads the file without that setting, and an install that stops writing the map removes the one a previous install left.
90
+
91
+ #### Windows
92
+
93
+ - `pnpm pipeline` no longer fails with intermittent access denied errors when concurrent tasks save their cache entries on Windows.
94
+
95
+ - Windows filesystem operations now retry permission errors for up to one second [#14682](https://github.com/pnpm/pnpm/issues/14682). A permanent permission error delayed the failure by a minute. Sharing and lock violations keep their one minute retry budget.
96
+
97
+ #### Messages and output
98
+
99
+ - pnpm now warns when the root `package.json` declares a non-empty `workspaces` array and the project has no `pnpm-workspace.yaml` [#2255](https://github.com/pnpm/pnpm/issues/2255). Such an install linked no project and said nothing about why.
100
+
101
+ - `ERR_PNPM_PACKAGE_MANAGER_REMOVE_MODULES_DIR` now names the file or directory in `node_modules` that pnpm could not clean up. It reported only the underlying OS error, such as "Access is denied (os error 5)".
102
+
103
+ - `pnpm --help` no longer describes pnpm as experimental.
104
+
105
+ ## 12.4.0
106
+
107
+ ### Minor Changes
108
+
109
+ - pnpm can now manage npm, Python, and Cargo dependencies in the same workspace. Enable `python.enabled` or `cargo.enabled` in `pnpm-workspace.yaml`, then use `pnpm install` to install them together.
110
+
111
+ - Add Python packages with `pnpm add pypi:<package>`. pnpm uses `pyproject.toml`, `pylock.toml`, and a managed `.venv`. Frozen and offline installs are supported, and `pnpm run` and `pnpm exec` make the environment's executables available [#14566](https://github.com/pnpm/pnpm/issues/14566).
112
+ - Add Rust crates with `pnpm add crate:<package>`. pnpm supports crates.io and custom sparse registries configured with `cargo.indexUrl`. Registry authentication supports pnpm credentials and, for crates.io, `CARGO_REGISTRY_TOKEN` or `$CARGO_HOME/credentials.toml`.
113
+
114
+ Both ecosystems support faster dependency resolution through `pnprServer`, with local resolution as a fallback when the server does not support it.
115
+
116
+ - Added `pnpm pipeline [name]` to install frozen dependencies and run workspace tasks declared in `pipelines`. It selects affected projects, runs their task graph, and continues running tasks after a task fails.
117
+
118
+ Tasks support `inputs`, `outputs`, `env`, and `cache` settings. Cached results restore task outputs and replay logs. Cargo tasks can reuse local build state between worktrees with `tasks.<name>.cargoTargetDir`. Set `includeWorkspaceRoot: true` to include root tasks.
119
+
120
+ Use `pnpm pipeline --dry-run` to preview the task graph without installing configuration dependencies or running workspace hooks.
121
+
122
+ - Added support for Android on arm64 and x64, FreeBSD on x64, and Linux on ppc64le, s390x, and RISC-V (riscv64 with glibc) [#14431](https://github.com/pnpm/pnpm/issues/14431), [#14597](https://github.com/pnpm/pnpm/issues/14597), [#7582](https://github.com/pnpm/pnpm/issues/7582).
123
+
124
+ - Added `trustPolicyExcludePrune` to automatically remove unused versions and packages from `trustPolicyExclude` when running `pnpm add`, `pnpm update`, or `pnpm remove`. It is disabled by default. Package name patterns such as `@scope/*` are kept, and cleanup is skipped when `sharedWorkspaceLockfile` is `false`.
125
+
126
+ - Added `pnpm change check` for CI validation of package versions against the `versioning.epics` bands and `versioning.fixed` groups in `pnpm-workspace.yaml`. It reports all violations, including packages that are not part of the current release.
127
+
128
+ ### Patch Changes
129
+
130
+ - Registry metadata is now kept separate for registries with different URL paths or schemes. This prevents installs from using another registry's package versions or tarball URLs, and keeps metadata fetched over HTTP from being reused for HTTPS [#13558](https://github.com/pnpm/pnpm/issues/13558).
131
+
132
+ The first install after upgrading refetches registry metadata. The package store is unchanged. `pnpm cache view` now shows full registry URLs. Scripts that parse the directory names from `pnpm cache list-registries` or `pnpm cache list` need updating.
133
+
134
+ - Patches that add build scripts or a `binding.gyp` now trigger a build, subject to build approval. Unapproved builds appear under "Ignored build scripts" [#14648](https://github.com/pnpm/pnpm/issues/14648).
135
+
136
+ - Build scripts can now be rejected before installing a package with `pnpm add --allow-build=!<pkg>`, including global installs. `pnpm approve-builds <pkg>` and `pnpm approve-builds !<pkg>` also save decisions when no packages are awaiting approval. They warn if the named package is not awaiting approval [#14067](https://github.com/pnpm/pnpm/issues/14067).
137
+
138
+ - A registry configured in `.npmrc` now takes precedence over registry settings saved by `pnpm login` in the global `config.yaml`. This fixes installs using the wrong registry after login [#14614](https://github.com/pnpm/pnpm/issues/14614).
139
+
140
+ - Large downloads over slow connections no longer time out while data is still arriving. `fetch-timeout` now limits how long a request can go without making progress [#14604](https://github.com/pnpm/pnpm/issues/14604).
141
+
142
+ - Sped up installs in workspaces with many projects when reusing a warm global virtual store [#14540](https://github.com/pnpm/pnpm/issues/14540).
143
+
144
+ - `pnpm deploy` is faster in large workspaces and no longer fails with `ERR_PNPM_LOCKFILE_CONFIG_MISMATCH` when the project includes a `.pnpmfile.mjs` [#14539](https://github.com/pnpm/pnpm/issues/14539), [#14671](https://github.com/pnpm/pnpm/issues/14671).
145
+
146
+ - `pnpm add --workspace <pkg>` works again. It saves the dependency with the `workspace:` protocol and links it from the workspace. The command fails if no workspace project provides the package [#14602](https://github.com/pnpm/pnpm/issues/14602).
147
+
148
+ - `pnpm add` and `pnpm install` now accept protocol-prefixed selectors such as `jsr:@scope/pkg`, `npm:pkg@^1.0.0`, and `workspace:pkg@*` [#14590](https://github.com/pnpm/pnpm/issues/14590). Installs with JSR dependencies in the lockfile also no longer fail with `ERR_PNPM_META_FETCH_FAIL` [#14649](https://github.com/pnpm/pnpm/issues/14649).
149
+
150
+ - Boolean flags now accept explicit inline values. For example, `pnpm install --prod=false` installs devDependencies, while `--prod=true` skips them [#14553](https://github.com/pnpm/pnpm/issues/14553).
151
+
152
+ - `pnpm install <pkg>` now accepts `--offline` and `--prefer-offline`, as `pnpm add <pkg>` already did [#14194](https://github.com/pnpm/pnpm/pull/14194).
153
+
154
+ - Fixed `pnpm install --frozen-lockfile` rejecting a freshly generated lockfile when overrides use relative `file:` or `link:` paths in a workspace [#14555](https://github.com/pnpm/pnpm/issues/14555).
155
+
156
+ - Fixed installs with config dependencies failing on symlinked lockfiles, such as those used by Bazel and Nix, when the config dependencies have not changed. Updates that would write through a symlink remain disallowed. Updating config dependencies also preserves lockfiles that start with a byte order mark [#14372](https://github.com/pnpm/pnpm/issues/14372).
157
+
158
+ - Fixed package manager version pins being written to the wrong lockfile when `lockfileDir` is set. The pins also remain consistent across commands when version switching is disabled, avoiding unnecessary lockfile changes [#14633](https://github.com/pnpm/pnpm/issues/14633), [#14575](https://github.com/pnpm/pnpm/issues/14575).
159
+
160
+ - `pnpm import` now respects `lockfileDir` and branch lockfiles without modifying other lockfiles. Failed imports restore the destination lockfile [#14563](https://github.com/pnpm/pnpm/issues/14563).
161
+
162
+ - `pnpm patch-commit` now produces valid patches when files are added or deleted. `pnpm install` also accepts patches that delete files without listing their contents, and patch files with CRLF line endings [#14559](https://github.com/pnpm/pnpm/issues/14559), [#14557](https://github.com/pnpm/pnpm/issues/14557).
163
+
164
+ - Fixed version ranges with partial upper bounds. For example, `<=16` now includes all 16.x versions, and `>=0.11 <=3` correctly accepts 3.0.1 [#14419](https://github.com/pnpm/pnpm/issues/14419).
165
+
166
+ - Workspace package patterns now support `.` and `..` segments and repeated slashes. Patterns such as `./packages/*` and exclusions such as `!./packages/foo` now match correctly [#14571](https://github.com/pnpm/pnpm/issues/14571).
167
+
168
+ - `packageConfigs` settings now apply to the specified projects when `sharedWorkspaceLockfile` is `false`, including `overrides`, `hoist`, `modulesDir`, `saveExact`, and `savePrefix`. Workspaces with a shared lockfile report which entries were ignored [#14556](https://github.com/pnpm/pnpm/issues/14556).
169
+
170
+ - `pnpm run` and `pnpm exec` no longer report a changed workspace structure after a successful install when `sharedWorkspaceLockfile` is `false` and `verifyDepsBeforeRun` is enabled [#14588](https://github.com/pnpm/pnpm/issues/14588).
171
+
172
+ - Commands run from a project's subdirectory now find the nearest ancestor with a manifest. This fixes commands such as `pnpm bin` returning paths under the wrong directory. `pnpm init` still creates its manifest in the current directory, and `pnpm exec` still runs there [#14622](https://github.com/pnpm/pnpm/issues/14622).
173
+
174
+ - Relative `scriptShell` paths in `pnpm-workspace.yaml` now resolve from the workspace root, including when scripts run in nested packages. Bare command names such as `bash` still use `PATH` [#14422](https://github.com/pnpm/pnpm/issues/14422).
175
+
176
+ - Fixed installing the pnpm version pinned in `packageManager` when `nodeLinker` is `hoisted`. Managed Node.js, Deno, and Bun installations also work when the global config uses `nodeLinker: hoisted` [#14595](https://github.com/pnpm/pnpm/issues/14595).
177
+
178
+ - The JavaScript pnpm can again switch to a project's pinned pnpm version on platforms without a native binary for that version, such as Alpine Linux with pnpm 10 or Intel Macs with pnpm 11. If a native pnpm version does not support the platform, the error now names the missing target [#13622](https://github.com/pnpm/pnpm/issues/13622).
179
+
180
+ - Provisioning Yarn 6 now uses `GH_TOKEN` or `GITHUB_TOKEN` when available to avoid GitHub's anonymous API rate limit in CI. Tokens are only sent when `strict-ssl` is enabled.
181
+
182
+ - Fixed concurrent installs sharing a global virtual store on macOS failing with "failed to import ... No such file or directory" [#14560](https://github.com/pnpm/pnpm/issues/14560).
183
+
184
+ - Fixed `pnpm setup` failing with `ERR_PNPM_DIRECTORY_FETCHER_PATH_ESCAPE` on Windows. Local `file:` dependencies whose directories are symlinks or junctions are now packed correctly [#14618](https://github.com/pnpm/pnpm/issues/14618).
185
+
186
+ - On Windows, installs now retry replacing command shims temporarily locked by another process [#14549](https://github.com/pnpm/pnpm/issues/14549).
187
+
188
+ - Fixed argument forwarding on Windows with `shellEmulator` enabled. Paths ending in a backslash, line breaks, and literal shell expressions are preserved [#14548](https://github.com/pnpm/pnpm/issues/14548).
189
+
190
+ - Windows store paths now consistently use backslashes in `pnpm store path` output and in the `storeDir` and `virtualStoreDir` fields of `node_modules/.modules.yaml`.
191
+
192
+ - Invalid certificates in `ca` or `cafile` no longer cause an `Invalid CA certificate` error. Valid certificates still apply, and blank `cert` or `key` values are treated as unset [#14646](https://github.com/pnpm/pnpm/issues/14646).
193
+
194
+ - Installs now respect the archive extraction concurrency limit even after a download is abandoned [#14585](https://github.com/pnpm/pnpm/issues/14585).
195
+
196
+ - `pnpm audit` summaries now exclude advisories ignored through `auditConfig.ignoreGhsas` and report them separately. When all advisories are ignored, the summary says so [#14535](https://github.com/pnpm/pnpm/issues/14535).
197
+
198
+ - `pnpm pack --json` now reports errors as JSON. Lifecycle script output appears before the final JSON output.
199
+
200
+ - `pnpm outdated -r` now wraps the `Dependents` column, keeping the table readable when many workspace projects use the same dependency [#14591](https://github.com/pnpm/pnpm/issues/14591).
201
+
202
+ - Shell completions now support the `pn` alias in bash, fish, pwsh, and zsh [#11955](https://github.com/pnpm/pnpm/issues/11955).
203
+
204
+ - `pnpm version` now accepts `-m` as a short alias for `--message` [#14567](https://github.com/pnpm/pnpm/issues/14567).
205
+
3
206
  ## 12.3.4
4
207
 
5
208
  ### Patch Changes
package/README.md CHANGED
@@ -52,11 +52,16 @@ To quote the [Rush](https://rushjs.io/) team:
52
52
  </a>
53
53
  </td>
54
54
  <td align="center" valign="middle">
55
- <a href="https://notion.com/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer">
55
+ <a href="https://notion.com/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/notion.svg" width="80" alt="Notion"></a>
56
+ </td>
57
+ </tr>
58
+ <tr>
59
+ <td align="center" valign="middle">
60
+ <a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer">
56
61
  <picture>
57
- <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/notion.svg" />
58
- <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/notion_light.svg" />
59
- <img src="https://pnpm.io/img/users/notion.svg" width="80" alt="Notion" />
62
+ <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
63
+ <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
64
+ <img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
60
65
  </picture>
61
66
  </a>
62
67
  </td>
@@ -101,15 +106,6 @@ To quote the [Rush](https://rushjs.io/) team:
101
106
  </picture>
102
107
  </a>
103
108
  </td>
104
- <td align="center" valign="middle">
105
- <a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer">
106
- <picture>
107
- <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
108
- <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
109
- <img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
110
- </picture>
111
- </a>
112
- </td>
113
109
  <td align="center" valign="middle">
114
110
  <a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer">
115
111
  <picture>
@@ -119,8 +115,6 @@ To quote the [Rush](https://rushjs.io/) team:
119
115
  </picture>
120
116
  </a>
121
117
  </td>
122
- </tr>
123
- <tr>
124
118
  <td align="center" valign="middle">
125
119
  <a href="https://workleap.com/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer">
126
120
  <picture>
@@ -130,6 +124,8 @@ To quote the [Rush](https://rushjs.io/) team:
130
124
  </picture>
131
125
  </a>
132
126
  </td>
127
+ </tr>
128
+ <tr>
133
129
  <td align="center" valign="middle">
134
130
  <a href="https://nx.dev/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer">
135
131
  <picture>
@@ -1,9 +1,9 @@
1
1
  {
2
- "lastValidatedTimestamp": 1788529665777,
2
+ "lastValidatedTimestamp": 1789057805684,
3
3
  "projects": {
4
4
  "/home/runner/_work/pnpm/pnpm/pnpm/npm/pnpm/temp-deploy": {
5
5
  "name": "pacquet",
6
- "version": "12.3.4"
6
+ "version": "12.4.1"
7
7
  }
8
8
  },
9
9
  "pnpmfiles": [],
@@ -28,7 +28,7 @@
28
28
  "@commitlint/prompt-cli": "^21.2.2",
29
29
  "@cyclonedx/cyclonedx-library": "10.2.0",
30
30
  "@eslint/js": "^10.0.1",
31
- "@inquirer/prompts": "^8.7.1",
31
+ "@inquirer/prompts": "^8.7.2",
32
32
  "@jest/globals": "30.4.1",
33
33
  "@npm/types": "^2.1.0",
34
34
  "@openpgp/web-stream-tools": "0.3.1",
@@ -48,7 +48,7 @@
48
48
  "@pnpm/tabtab": "^0.5.4",
49
49
  "@pnpm/tgz-fixtures": "0.0.0",
50
50
  "@reflink/reflink": "0.1.19",
51
- "@rushstack/worker-pool": "0.7.25",
51
+ "@rushstack/worker-pool": "0.7.26",
52
52
  "@stylistic/eslint-plugin": "^5.10.0",
53
53
  "@types/adm-zip": "^0.5.8",
54
54
  "@types/archy": "0.0.36",
@@ -87,7 +87,7 @@
87
87
  "@types/write-file-atomic": "^4.0.3",
88
88
  "@types/yarnpkg__lockfile": "^1.1.9",
89
89
  "@types/zkochan__table": "npm:@types/table@6.3.2",
90
- "@typescript-eslint/utils": "^8.69.0",
90
+ "@typescript-eslint/utils": "^8.70.0",
91
91
  "@typescript/native-preview": "7.0.0-dev.20260707.2",
92
92
  "@yarnpkg/core": "4.9.1",
93
93
  "@yarnpkg/extensions": "2.0.7",
@@ -127,10 +127,9 @@
127
127
  "dint": "^5.1.0",
128
128
  "dir-is-case-sensitive": "^3.0.0",
129
129
  "empathic": "^2.0.1",
130
- "encode-registry": "^3.0.1",
131
130
  "esbuild": "^0.28.2",
132
131
  "escape-string-regexp": "^5.0.0",
133
- "eslint": "^10.9.1",
132
+ "eslint": "^10.10.0",
134
133
  "eslint-plugin-import-x": "^4.17.1",
135
134
  "eslint-plugin-jest": "^29.16.6",
136
135
  "eslint-plugin-n": "^18.3.0",
@@ -200,7 +199,7 @@
200
199
  "path-temp": "^3.0.0",
201
200
  "pidtree": "^1.0.0",
202
201
  "preferred-pm": "^5.0.0",
203
- "pretty-bytes": "^7.1.2",
202
+ "pretty-bytes": "^7.1.3",
204
203
  "pretty-ms": "^9.3.1",
205
204
  "promise-share": "^2.0.1",
206
205
  "proxyquire": "^2.1.3",
@@ -247,7 +246,7 @@
247
246
  "tree-kill": "^1.2.2",
248
247
  "ts-jest-resolver": "2.0.1",
249
248
  "typescript": "6.0.3",
250
- "typescript-eslint": "^8.69.0",
249
+ "typescript-eslint": "^8.70.0",
251
250
  "undici": "^7.29.0",
252
251
  "unified": "^11.0.5",
253
252
  "validate-npm-package-name": "7.0.2",
@@ -320,6 +319,7 @@
320
319
  "nodeLinker": "hoisted",
321
320
  "optional": true,
322
321
  "patchedDependencies": {
322
+ "adm-zip@0.6.0": "../../../../__patches__/adm-zip@0.6.0.patch",
323
323
  "graceful-fs@4.2.11": "../../../../__patches__/graceful-fs@4.2.11.patch",
324
324
  "normalize-registry-url@2.0.1": "../../../../__patches__/normalize-registry-url@2.0.1.patch"
325
325
  },
@@ -331,10 +331,8 @@
331
331
  "trustPolicyExclude": [
332
332
  "@pnpm/*",
333
333
  "@yarnpkg/libzip@3.2.2",
334
- "fastq@1.20.2",
335
334
  "pnpm",
336
335
  "rxjs@7.8.2",
337
- "tinyexec@1.2.2",
338
336
  "undici-types@6.21.0"
339
337
  ],
340
338
  "trustPolicyIgnoreAfter": 10080
@@ -876,7 +876,7 @@ async function connectH1 (client, socket) {
876
876
 
877
877
  function clearIdleSocketValidation (socket) {
878
878
  if (socket[kIdleSocketValidationTimeout]) {
879
- clearTimeout(socket[kIdleSocketValidationTimeout])
879
+ clearImmediate(socket[kIdleSocketValidationTimeout])
880
880
  socket[kIdleSocketValidationTimeout] = null
881
881
  }
882
882
 
@@ -885,15 +885,23 @@ function clearIdleSocketValidation (socket) {
885
885
 
886
886
  function scheduleIdleSocketValidation (client, socket) {
887
887
  socket[kIdleSocketValidation] = 1
888
- socket[kIdleSocketValidationTimeout] = setTimeout(() => {
888
+ // Yield to the check phase (after poll) so unsolicited bytes / FIN / RST
889
+ // already pending on this idle keep-alive socket are processed before the
890
+ // next request is written (GHSA-35p6-xmwp-9g52).
891
+ //
892
+ // setTimeout(0) pays Node's ~1ms timer floor on every sequential reuse
893
+ // (#5493). setImmediate avoids that, but an *unref'd* Immediate lets poll
894
+ // block for ~500ms when the event loop is otherwise idle (#5600 / #5606).
895
+ // A ref'd Immediate both keeps the pending request alive and makes poll
896
+ // return immediately — the hybrid those issues asked for.
897
+ socket[kIdleSocketValidationTimeout] = setImmediate(() => {
889
898
  socket[kIdleSocketValidationTimeout] = null
890
899
  socket[kIdleSocketValidation] = 2
891
900
 
892
901
  if (client[kSocket] === socket && !socket.destroyed) {
893
902
  client[kResume]()
894
903
  }
895
- }, 0)
896
- socket[kIdleSocketValidationTimeout].unref?.()
904
+ })
897
905
  }
898
906
 
899
907
  /**
@@ -90,6 +90,7 @@ class RetryHandler {
90
90
  this.end = null
91
91
  this.etag = null
92
92
  this.resume = null
93
+ this.headersSent = false
93
94
 
94
95
  // Handle possible onConnect duplication
95
96
  this.handler.onConnect(reason => {
@@ -102,6 +103,20 @@ class RetryHandler {
102
103
  })
103
104
  }
104
105
 
106
+ checkpointResponseEnd (headers, resume) {
107
+ if (this.end == null && this.opts.method !== 'HEAD') {
108
+ const contentLength = headers['content-length']
109
+ this.end = contentLength != null ? Number(contentLength) - 1 : null
110
+
111
+ assert(
112
+ this.end == null || Number.isFinite(this.end),
113
+ 'invalid content-length'
114
+ )
115
+ }
116
+
117
+ this.resume = this.end != null ? resume : null
118
+ }
119
+
105
120
  onRequestSent () {
106
121
  if (this.handler.onRequestSent) {
107
122
  this.handler.onRequestSent()
@@ -191,6 +206,8 @@ class RetryHandler {
191
206
 
192
207
  if (statusCode >= 300) {
193
208
  if (this.retryOpts.statusCodes.includes(statusCode) === false) {
209
+ this.headersSent = true
210
+ this.checkpointResponseEnd(headers, resume)
194
211
  return this.handler.onHeaders(
195
212
  statusCode,
196
213
  rawHeaders,
@@ -259,8 +276,15 @@ class RetryHandler {
259
276
 
260
277
  const { start, size, end = size - 1 } = contentRange
261
278
 
262
- assert(this.start === start, 'content-range mismatch')
263
- assert(this.end == null || this.end === end, 'content-range mismatch')
279
+ if (this.start !== start || (this.end != null && this.end !== end)) {
280
+ this.abort(
281
+ new RequestRetryError('Content-Range mismatch', statusCode, {
282
+ headers,
283
+ data: { count: this.retryCount }
284
+ })
285
+ )
286
+ return false
287
+ }
264
288
 
265
289
  this.resume = resume
266
290
  return true
@@ -272,6 +296,7 @@ class RetryHandler {
272
296
  const range = parseRangeHeader(headers['content-range'])
273
297
 
274
298
  if (range == null) {
299
+ this.headersSent = true
275
300
  return this.handler.onHeaders(
276
301
  statusCode,
277
302
  rawHeaders,
@@ -310,6 +335,7 @@ class RetryHandler {
310
335
  )
311
336
 
312
337
  this.resume = resume
338
+ this.headersSent = true
313
339
  this.etag = headers.etag != null ? headers.etag : null
314
340
 
315
341
  // Weak etags are not useful for comparison nor cache
@@ -349,7 +375,7 @@ class RetryHandler {
349
375
  }
350
376
 
351
377
  onError (err) {
352
- if (this.aborted || isDisturbed(this.opts.body)) {
378
+ if (this.aborted || isDisturbed(this.opts.body) || (this.headersSent && this.resume == null)) {
353
379
  return this.handler.onError(err)
354
380
  }
355
381
 
@@ -23,6 +23,49 @@ const COLON = 0x3A
23
23
  */
24
24
  const SPACE = 0x20
25
25
 
26
+ const DATA = Buffer.from('data')
27
+ const EVENT = Buffer.from('event')
28
+ const ID = Buffer.from('id')
29
+ const RETRY = Buffer.from('retry')
30
+
31
+ function isASCIINumberBytes (buffer, start) {
32
+ if (start >= buffer.length) {
33
+ return false
34
+ }
35
+
36
+ for (let i = start; i < buffer.length; i++) {
37
+ if (buffer[i] < 0x30 || buffer[i] > 0x39) {
38
+ return false
39
+ }
40
+ }
41
+
42
+ return true
43
+ }
44
+
45
+ function isValidLastEventIdBytes (buffer, start) {
46
+ for (let i = start; i < buffer.length; i++) {
47
+ if (buffer[i] === 0x00) {
48
+ return false
49
+ }
50
+ }
51
+
52
+ return true
53
+ }
54
+
55
+ function isFieldName (line, length, field) {
56
+ if (length !== field.length) {
57
+ return false
58
+ }
59
+
60
+ for (let i = 0; i < length; i++) {
61
+ if (line[i] !== field[i]) {
62
+ return false
63
+ }
64
+ }
65
+
66
+ return true
67
+ }
68
+
26
69
  /**
27
70
  * @typedef {object} EventSourceStreamEvent
28
71
  * @type {object}
@@ -63,11 +106,14 @@ class EventSourceStream extends Transform {
63
106
  eventEndCheck = false
64
107
 
65
108
  /**
66
- * @type {Buffer}
109
+ * @type {Buffer[]}
67
110
  */
68
- buffer = null
111
+ chunks = []
69
112
 
113
+ chunkIndex = 0
70
114
  pos = 0
115
+ lineChunkIndex = 0
116
+ linePos = 0
71
117
 
72
118
  event = {
73
119
  data: undefined,
@@ -106,92 +152,20 @@ class EventSourceStream extends Transform {
106
152
  return
107
153
  }
108
154
 
109
- // Cache the chunk in the buffer, as the data might not be complete while
110
- // processing it
111
- // TODO: Investigate if there is a more performant way to handle
112
- // incoming chunks
113
- // see: https://github.com/nodejs/undici/issues/2630
114
- if (this.buffer) {
115
- this.buffer = Buffer.concat([this.buffer, chunk])
116
- } else {
117
- this.buffer = chunk
118
- }
155
+ this.chunks.push(chunk)
119
156
 
120
157
  // Strip leading byte-order-mark if we opened the stream and started
121
158
  // the processing of the incoming data
122
159
  if (this.checkBOM) {
123
- switch (this.buffer.length) {
124
- case 1:
125
- // Check if the first byte is the same as the first byte of the BOM
126
- if (this.buffer[0] === BOM[0]) {
127
- // If it is, we need to wait for more data
128
- callback()
129
- return
130
- }
131
- // Set the checkBOM flag to false as we don't need to check for the
132
- // BOM anymore
133
- this.checkBOM = false
134
-
135
- // The buffer only contains one byte so we need to wait for more data
136
- callback()
137
- return
138
- case 2:
139
- // Check if the first two bytes are the same as the first two bytes
140
- // of the BOM
141
- if (
142
- this.buffer[0] === BOM[0] &&
143
- this.buffer[1] === BOM[1]
144
- ) {
145
- // If it is, we need to wait for more data, because the third byte
146
- // is needed to determine if it is the BOM or not
147
- callback()
148
- return
149
- }
150
-
151
- // Set the checkBOM flag to false as we don't need to check for the
152
- // BOM anymore
153
- this.checkBOM = false
154
- break
155
- case 3:
156
- // Check if the first three bytes are the same as the first three
157
- // bytes of the BOM
158
- if (
159
- this.buffer[0] === BOM[0] &&
160
- this.buffer[1] === BOM[1] &&
161
- this.buffer[2] === BOM[2]
162
- ) {
163
- // If it is, we can drop the buffered data, as it is only the BOM
164
- this.buffer = Buffer.alloc(0)
165
- // Set the checkBOM flag to false as we don't need to check for the
166
- // BOM anymore
167
- this.checkBOM = false
168
-
169
- // Await more data
170
- callback()
171
- return
172
- }
173
- // If it is not the BOM, we can start processing the data
174
- this.checkBOM = false
175
- break
176
- default:
177
- // The buffer is longer than 3 bytes, so we can drop the BOM if it is
178
- // present
179
- if (
180
- this.buffer[0] === BOM[0] &&
181
- this.buffer[1] === BOM[1] &&
182
- this.buffer[2] === BOM[2]
183
- ) {
184
- // Remove the BOM from the buffer
185
- this.buffer = this.buffer.subarray(3)
186
- }
187
-
188
- // Set the checkBOM flag to false as we don't need to check for the
189
- this.checkBOM = false
190
- break
160
+ if (this.handleBOM()) {
161
+ callback()
162
+ return
191
163
  }
192
164
  }
193
165
 
194
- while (this.pos < this.buffer.length) {
166
+ while (this.hasCurrentByte()) {
167
+ const byte = this.currentByte()
168
+
195
169
  // If the previous line ended with an end-of-line, we need to check
196
170
  // if the next character is also an end-of-line.
197
171
  if (this.eventEndCheck) {
@@ -204,10 +178,9 @@ class EventSourceStream extends Transform {
204
178
  if (this.crlfCheck) {
205
179
  // If the current character is a line feed, we can remove it
206
180
  // from the buffer and reset the crlfCheck flag
207
- if (this.buffer[this.pos] === LF) {
208
- this.buffer = this.buffer.subarray(this.pos + 1)
209
- this.pos = 0
181
+ if (byte === LF) {
210
182
  this.crlfCheck = false
183
+ this.consumeCurrentByte()
211
184
 
212
185
  // It is possible that the line feed is not the end of the
213
186
  // event. We need to check if the next character is an
@@ -223,19 +196,17 @@ class EventSourceStream extends Transform {
223
196
  this.crlfCheck = false
224
197
  }
225
198
 
226
- if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) {
199
+ if (byte === LF || byte === CR) {
227
200
  // If the current character is a carriage return, we need to
228
201
  // set the crlfCheck flag to true, as we need to check if the
229
202
  // next character is a line feed so we can remove it from the
230
203
  // buffer
231
- if (this.buffer[this.pos] === CR) {
204
+ if (byte === CR) {
232
205
  this.crlfCheck = true
233
206
  }
234
207
 
235
- this.buffer = this.buffer.subarray(this.pos + 1)
236
- this.pos = 0
237
- if (
238
- this.event.data !== undefined || this.event.event || this.event.id || this.event.retry) {
208
+ this.consumeCurrentByte()
209
+ if (this.hasPendingEvent()) {
239
210
  this.processEvent(this.event)
240
211
  }
241
212
  this.clearEvent()
@@ -249,22 +220,18 @@ class EventSourceStream extends Transform {
249
220
 
250
221
  // If the current character is an end-of-line, we can process the
251
222
  // line
252
- if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) {
223
+ if (byte === LF || byte === CR) {
253
224
  // If the current character is a carriage return, we need to
254
225
  // set the crlfCheck flag to true, as we need to check if the
255
226
  // next character is a line feed
256
- if (this.buffer[this.pos] === CR) {
227
+ if (byte === CR) {
257
228
  this.crlfCheck = true
258
229
  }
259
230
 
260
231
  // In any case, we can process the line as we reached an
261
232
  // end-of-line character
262
- this.parseLine(this.buffer.subarray(0, this.pos), this.event)
263
-
264
- // Remove the processed line from the buffer
265
- this.buffer = this.buffer.subarray(this.pos + 1)
266
- // Reset the position as we removed the processed line from the buffer
267
- this.pos = 0
233
+ this.parseLine(this.readLine(), this.event)
234
+ this.consumeCurrentByte()
268
235
  // A line was processed and this could be the end of the event. We need
269
236
  // to check if the next line is empty to determine if the event is
270
237
  // finished.
@@ -272,7 +239,7 @@ class EventSourceStream extends Transform {
272
239
  continue
273
240
  }
274
241
 
275
- this.pos++
242
+ this.advanceCursor()
276
243
  }
277
244
 
278
245
  callback()
@@ -297,64 +264,53 @@ class EventSourceStream extends Transform {
297
264
  return
298
265
  }
299
266
 
300
- let field = ''
301
- let value = ''
267
+ let fieldLength = line.length
268
+ let valueStart = line.length
302
269
 
303
270
  // If the line contains a U+003A COLON character (:)
304
271
  if (colonPosition !== -1) {
305
- // Collect the characters on the line before the first U+003A COLON
306
- // character (:), and let field be that string.
307
- // TODO: Investigate if there is a more performant way to extract the
308
- // field
309
- // see: https://github.com/nodejs/undici/issues/2630
310
- field = line.subarray(0, colonPosition).toString('utf8')
272
+ fieldLength = colonPosition
311
273
 
312
274
  // Collect the characters on the line after the first U+003A COLON
313
275
  // character (:), and let value be that string.
314
276
  // If value starts with a U+0020 SPACE character, remove it from value.
315
- let valueStart = colonPosition + 1
277
+ valueStart = colonPosition + 1
316
278
  if (line[valueStart] === SPACE) {
317
279
  ++valueStart
318
280
  }
319
- // TODO: Investigate if there is a more performant way to extract the
320
- // value
321
- // see: https://github.com/nodejs/undici/issues/2630
322
- value = line.subarray(valueStart).toString('utf8')
323
-
324
- // Otherwise, the string is not empty but does not contain a U+003A COLON
325
- // character (:)
326
- } else {
327
- // Process the field using the steps described below, using the whole
328
- // line as the field name, and the empty string as the field value.
329
- field = line.toString('utf8')
330
- value = ''
331
281
  }
332
282
 
333
- // Modify the event with the field name and value. The value is also
334
- // decoded as UTF-8
335
- switch (field) {
336
- case 'data':
337
- if (event[field] === undefined) {
338
- event[field] = value
339
- } else {
340
- event[field] += `\n${value}`
341
- }
342
- break
343
- case 'retry':
344
- if (isASCIINumber(value)) {
345
- event[field] = value
346
- }
347
- break
348
- case 'id':
349
- if (isValidLastEventId(value)) {
350
- event[field] = value
351
- }
352
- break
353
- case 'event':
354
- if (value.length > 0) {
355
- event[field] = value
356
- }
357
- break
283
+ if (isFieldName(line, fieldLength, DATA)) {
284
+ const value = line.toString('utf8', valueStart)
285
+
286
+ if (event.data === undefined) {
287
+ event.data = value
288
+ } else {
289
+ event.data += `\n${value}`
290
+ }
291
+ return
292
+ }
293
+
294
+ if (isFieldName(line, fieldLength, RETRY)) {
295
+ if (isASCIINumberBytes(line, valueStart)) {
296
+ event.retry = line.toString('utf8', valueStart)
297
+ }
298
+ return
299
+ }
300
+
301
+ if (isFieldName(line, fieldLength, ID)) {
302
+ if (isValidLastEventIdBytes(line, valueStart)) {
303
+ event.id = line.toString('utf8', valueStart)
304
+ }
305
+ return
306
+ }
307
+
308
+ if (isFieldName(line, fieldLength, EVENT)) {
309
+ const value = line.toString('utf8', valueStart)
310
+
311
+ if (value.length > 0) {
312
+ event.event = value
313
+ }
358
314
  }
359
315
  }
360
316
 
@@ -384,12 +340,151 @@ class EventSourceStream extends Transform {
384
340
  }
385
341
 
386
342
  clearEvent () {
387
- this.event = {
388
- data: undefined,
389
- event: undefined,
390
- id: undefined,
391
- retry: undefined
343
+ this.event.data = undefined
344
+ this.event.event = undefined
345
+ this.event.id = undefined
346
+ this.event.retry = undefined
347
+ }
348
+
349
+ hasPendingEvent () {
350
+ return this.event.data !== undefined ||
351
+ this.event.event !== undefined ||
352
+ this.event.id !== undefined ||
353
+ this.event.retry !== undefined
354
+ }
355
+
356
+ hasCurrentByte () {
357
+ return this.chunkIndex < this.chunks.length &&
358
+ this.pos < this.chunks[this.chunkIndex].length
359
+ }
360
+
361
+ currentByte () {
362
+ return this.chunks[this.chunkIndex][this.pos]
363
+ }
364
+
365
+ consumeCurrentByte () {
366
+ this.advanceCursor()
367
+ this.syncLineStartToCursor()
368
+ }
369
+
370
+ advanceCursor () {
371
+ this.pos++
372
+
373
+ while (this.chunkIndex < this.chunks.length && this.pos >= this.chunks[this.chunkIndex].length) {
374
+ this.chunkIndex++
375
+ this.pos = 0
376
+ }
377
+ }
378
+
379
+ syncLineStartToCursor () {
380
+ this.lineChunkIndex = this.chunkIndex
381
+ this.linePos = this.pos
382
+ this.dropConsumedChunks()
383
+ }
384
+
385
+ dropConsumedChunks () {
386
+ while (this.lineChunkIndex > 0) {
387
+ this.chunks.shift()
388
+ this.lineChunkIndex--
389
+ this.chunkIndex--
390
+ }
391
+
392
+ if (this.chunkIndex === this.chunks.length) {
393
+ this.chunks.length = 0
394
+ this.chunkIndex = 0
395
+ this.pos = 0
396
+ this.lineChunkIndex = 0
397
+ this.linePos = 0
398
+ }
399
+ }
400
+
401
+ readLine () {
402
+ if (this.lineChunkIndex === this.chunkIndex) {
403
+ return this.chunks[this.chunkIndex].subarray(this.linePos, this.pos)
404
+ }
405
+
406
+ const chunks = []
407
+ let length = 0
408
+
409
+ for (let i = this.lineChunkIndex; i <= this.chunkIndex; i++) {
410
+ const chunk = this.chunks[i]
411
+ const start = i === this.lineChunkIndex ? this.linePos : 0
412
+ const end = i === this.chunkIndex ? this.pos : chunk.length
413
+ const slice = chunk.subarray(start, end)
414
+ length += slice.length
415
+ chunks.push(slice)
416
+ }
417
+
418
+ return Buffer.concat(chunks, length)
419
+ }
420
+
421
+ peekBufferedByte (offset) {
422
+ let chunkIndex = this.lineChunkIndex
423
+ let pos = this.linePos
424
+
425
+ while (chunkIndex < this.chunks.length) {
426
+ const chunk = this.chunks[chunkIndex]
427
+ const remaining = chunk.length - pos
428
+
429
+ if (offset < remaining) {
430
+ return chunk[pos + offset]
431
+ }
432
+
433
+ offset -= remaining
434
+ chunkIndex++
435
+ pos = 0
436
+ }
437
+ }
438
+
439
+ discardLeadingBytes (count) {
440
+ while (count > 0 && this.lineChunkIndex < this.chunks.length) {
441
+ const chunk = this.chunks[this.lineChunkIndex]
442
+ const remaining = chunk.length - this.linePos
443
+
444
+ if (count < remaining) {
445
+ this.linePos += count
446
+ count = 0
447
+ } else {
448
+ count -= remaining
449
+ this.lineChunkIndex++
450
+ this.linePos = 0
451
+ }
452
+ }
453
+
454
+ this.chunkIndex = this.lineChunkIndex
455
+ this.pos = this.linePos
456
+ this.dropConsumedChunks()
457
+ }
458
+
459
+ handleBOM () {
460
+ const first = this.peekBufferedByte(0)
461
+ const second = this.peekBufferedByte(1)
462
+ const third = this.peekBufferedByte(2)
463
+
464
+ if (second === undefined) {
465
+ if (first === BOM[0]) {
466
+ return true
467
+ }
468
+
469
+ this.checkBOM = false
470
+ return true
471
+ }
472
+
473
+ if (third === undefined) {
474
+ if (first === BOM[0] && second === BOM[1]) {
475
+ return true
476
+ }
477
+
478
+ this.checkBOM = false
479
+ return false
392
480
  }
481
+
482
+ if (first === BOM[0] && second === BOM[1] && third === BOM[2]) {
483
+ this.discardLeadingBytes(3)
484
+ }
485
+
486
+ this.checkBOM = false
487
+ return !this.hasCurrentByte()
393
488
  }
394
489
  }
395
490
 
@@ -192,7 +192,7 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
192
192
  // is specified, the server needs to include the same field and one of
193
193
  // the selected subprotocol values in its response for the connection to
194
194
  // be established.
195
- if (!requestProtocols.includes(secProtocol)) {
195
+ if (requestProtocols === null || !requestProtocols.includes(secProtocol)) {
196
196
  failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.')
197
197
  return
198
198
  }
@@ -63,7 +63,12 @@ class PerMessageDeflate {
63
63
 
64
64
  if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) {
65
65
  callback(new MessageSizeExceededError())
66
+ // The inflater may still hold buffered input that can emit a late
67
+ // zlib error. Remove the data listener, then deterministically stop
68
+ // the stream so a subsequent 'error' cannot fire without a listener
69
+ // (which would terminate the process as an unhandled error event).
66
70
  this.#inflate.removeAllListeners()
71
+ this.#inflate.destroy()
67
72
  this.#inflate = null
68
73
  return
69
74
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "6.28.0",
3
+ "version": "6.28.1",
4
4
  "description": "An HTTP/1.1 client, written from scratch for Node.js",
5
5
  "homepage": "https://undici.nodejs.org",
6
6
  "bugs": {
package/install.js CHANGED
@@ -23,6 +23,7 @@ import path from 'node:path'
23
23
  import process from 'node:process'
24
24
  import {
25
25
  getBinCandidates,
26
+ hostTarget,
26
27
  readWrapperManifest,
27
28
  resolveInstalledBinary,
28
29
  splitBinSpecifier,
@@ -47,7 +48,7 @@ function setup () {
47
48
 
48
49
  const candidates = getBinCandidates()
49
50
  if (candidates.length === 0) {
50
- fail(`pnpm does not ship a prebuilt binary for ${process.platform}-${process.arch}.`)
51
+ fail(`pnpm does not ship a prebuilt binary for ${hostTarget()}.`)
51
52
  }
52
53
 
53
54
  const nativeBinary = resolveInstalledBinary()
package/native-binary.mjs CHANGED
@@ -2,6 +2,7 @@
2
2
  // (`install.js`), which links it over the placeholder bins, and by the Corepack
3
3
  // entry (`bin/pnpm.mjs`), which spawns it.
4
4
  import fs from 'node:fs'
5
+ import os from 'node:os'
5
6
  import path from 'node:path'
6
7
  import process from 'node:process'
7
8
  import { fileURLToPath } from 'node:url'
@@ -29,13 +30,35 @@ const PLATFORMS = {
29
30
  glibc: '@pnpm/exe.linux-arm64/pnpm',
30
31
  musl: '@pnpm/exe.linux-arm64-musl/pnpm',
31
32
  },
33
+ // Only a glibc build is released for these three.
34
+ riscv64: {
35
+ glibc: '@pnpm/exe.linux-riscv64/pnpm',
36
+ },
37
+ ppc64: {
38
+ glibc: '@pnpm/exe.linux-ppc64/pnpm',
39
+ },
40
+ s390x: {
41
+ glibc: '@pnpm/exe.linux-s390x/pnpm',
42
+ },
43
+ },
44
+ freebsd: {
45
+ x64: '@pnpm/exe.freebsd-x64/pnpm',
32
46
  },
47
+ // Android is bionic, which is neither of the two libcs the linux entries
48
+ // above are keyed on, so it takes a bare specifier of its own.
49
+ android: {
50
+ arm64: '@pnpm/exe.android-arm64/pnpm',
51
+ x64: '@pnpm/exe.android-x64/pnpm',
52
+ },
53
+ // Android is bionic, which is neither of the two libcs the linux entries
54
+ // above are keyed on, so it takes a bare specifier of its own.
33
55
  }
34
56
 
35
57
  /**
36
- * Native binary specifiers to try, most-preferred first; empty when the host is
37
- * unsupported. The linux glibc/musl pair is ordered by detected libc, which
38
- * only decides the winner when both are installed (e.g. `npm install --force`).
58
+ * Native binary specifiers to try, most-preferred first; empty when no released
59
+ * binary runs on the host. The linux glibc/musl pair is ordered by detected
60
+ * libc, which only decides the winner when both are installed (e.g.
61
+ * `npm install --force`).
39
62
  *
40
63
  * @returns {string[]}
41
64
  */
@@ -45,12 +68,36 @@ export function getBinCandidates () {
45
68
  if (platformEntry == null) {
46
69
  return []
47
70
  }
71
+ // Node reports both POWER endiannesses as `ppc64` and npm's `cpu` field
72
+ // cannot tell them apart, so a big-endian host installs the little-endian
73
+ // package it cannot run. Only the little-endian build is released.
74
+ if (arch === 'ppc64' && os.endianness() !== 'LE') {
75
+ return []
76
+ }
48
77
  if (typeof platformEntry === 'string') {
49
78
  return [platformEntry]
50
79
  }
51
80
 
52
- const order = detectLinuxLibc() === 'musl' ? ['musl', 'glibc'] : ['glibc', 'musl']
53
- return order.map((libc) => platformEntry[libc])
81
+ // An unprobeable libc (`detectLinuxLibc` returns null) counts as glibc.
82
+ const detected = detectLinuxLibc() === 'musl' ? 'musl' : 'glibc'
83
+ const preferred = platformEntry[detected]
84
+ // An architecture released for one libc only has no entry for the other, and
85
+ // the binary it does ship cannot run there, so it offers no candidate.
86
+ if (preferred == null) {
87
+ return []
88
+ }
89
+ const alternate = platformEntry[detected === 'musl' ? 'glibc' : 'musl']
90
+ return alternate == null ? [preferred] : [preferred, alternate]
91
+ }
92
+
93
+ /**
94
+ * How the host names itself in messages, spelled like the targets pnpm releases
95
+ * binaries for: `linux-x64-musl`, `darwin-arm64`.
96
+ *
97
+ * @returns {string}
98
+ */
99
+ export function hostTarget () {
100
+ return `${platform}-${arch}${detectLinuxLibc() === 'musl' ? '-musl' : ''}`
54
101
  }
55
102
 
56
103
  /**
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "./dist/node_modules/node-gyp/gyp/gyp"
10
10
  ]
11
11
  },
12
- "version": "12.3.4",
12
+ "version": "12.4.1",
13
13
  "description": "Fast, disk space efficient package manager",
14
14
  "keywords": [
15
15
  "pnpm",
@@ -46,14 +46,20 @@
46
46
  "THIRD-PARTY-NOTICES.md"
47
47
  ],
48
48
  "optionalDependencies": {
49
- "@pnpm/exe.win32-x64": "12.3.4",
50
- "@pnpm/exe.win32-arm64": "12.3.4",
51
- "@pnpm/exe.darwin-x64": "12.3.4",
52
- "@pnpm/exe.darwin-arm64": "12.3.4",
53
- "@pnpm/exe.linux-x64": "12.3.4",
54
- "@pnpm/exe.linux-arm64": "12.3.4",
55
- "@pnpm/exe.linux-x64-musl": "12.3.4",
56
- "@pnpm/exe.linux-arm64-musl": "12.3.4"
49
+ "@pnpm/exe.win32-x64": "12.4.1",
50
+ "@pnpm/exe.win32-arm64": "12.4.1",
51
+ "@pnpm/exe.darwin-x64": "12.4.1",
52
+ "@pnpm/exe.darwin-arm64": "12.4.1",
53
+ "@pnpm/exe.linux-x64": "12.4.1",
54
+ "@pnpm/exe.linux-arm64": "12.4.1",
55
+ "@pnpm/exe.linux-riscv64": "12.4.1",
56
+ "@pnpm/exe.linux-ppc64": "12.4.1",
57
+ "@pnpm/exe.linux-s390x": "12.4.1",
58
+ "@pnpm/exe.linux-x64-musl": "12.4.1",
59
+ "@pnpm/exe.linux-arm64-musl": "12.4.1",
60
+ "@pnpm/exe.freebsd-x64": "12.4.1",
61
+ "@pnpm/exe.android-arm64": "12.4.1",
62
+ "@pnpm/exe.android-x64": "12.4.1"
57
63
  },
58
64
  "scripts": {
59
65
  "preinstall": "node install.js",