Commit Graph
25 Commits
Author SHA1 Message Date
Zoltan Kochanandgithub-actions[bot] 91774e72a0 chore(release): 11.28.7, pacquet 12.15.0, pnpr 0.1.0-alpha.16 (#17187)
* chore(release): 11.28.7, pacquet 12.15.0, pnpr 0.1.0-alpha.16

* docs(release): curate 11.28.7, pacquet 12.15.0, and pnpr 0.1.0-alpha.16 release notes

Add lead paragraphs and group the patch entries by what the reader was
doing, with security fixes first on each page.

- v11: lead with the registry metadata name check, pinned pnpm
  trustPolicy check, maxTarballUnpackedSize, and the removal of registry
  revisions; merge overlapping entries for minimumReleaseAge metadata
  revalidation, frozen-lockfile directory dependency checks,
  syncInjectedDepsAfterScripts, verifyDepsBeforeRun, pnpm sbom, and
  unreadable config settings.
- v12: order minor highlights from config includes and
  lockfile.integrityTarget down to interactive update styling; drop the
  pnpr-only distribution note from the CLI changelog; merge overlapping
  entries for workspace and pnpr install speedups, minimumReleaseAge and
  If-Modified-Since revalidation, frozen-lockfile and injected package
  checks, verifyDepsBeforeRun, pnpm prune, pnpm pack, pnpm licenses
  list, pnpm self-update, pinned pnpm flag forwarding, and WebContainer
  fixes.
- pnpr: lead the minor section with the breaking ecosystem URL prefix,
  hosted storage layout, artifacts.orgs, and strict config schema
  changes; fold dist.pnpr tarball metadata into one entry and drop the
  redundant /metrics note; lead the patch section with reserved-username
  and auth security fixes.
- Use one issue-link label style across the release pages.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-21 12:15:12 +02:00
68112bada4 chore(release): 11.28.5, pacquet 12.10.0 (#16637)
* chore(release): 11.28.5, pacquet 12.10.0

* docs(release): curate the 11.28.5 and 12.10.0 release notes

Add a lead paragraph to each page and group the patch entries under
subject headings, with the security fixes first.

- Merge the unpublish path-boundary fix into the unpublish entry, the
  legacy metadata cache prune into the metadata cache entry, the two
  binary size entries, and the two 64 MiB archive limits on the v11 page.
- Drop the manifest-utils saveType entry from the CLI page: the CLI
  only passes saveType values derived from its own flags, so no CLI user
  can reach the fixed path.
- Rewrite mechanism-only entries (PAX header validation, bundled
  dependency symlinks, custom resolutions under name@version keys, proxy
  setting validation) in terms of what the user sees.
- Use one link style for pnpm/pnpm issues.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 02:00:03 +02:00
5dafb09800 chore(release): 11.28.4, pacquet 12.9.1 (#16567)
* chore(release): 11.28.4, pacquet 12.9.1

* docs(release): curate 11.28.4 and pacquet 12.9.1 release notes

Add a lead paragraph to both pages. Move the two credential-leak fixes
in 11.28.4 to the top and group the remaining 27 entries by what the
reader was doing. Merge entries that described one change: the two
INVALID_SETTING validations, the two metadata revalidation fixes, the
two hoisted filtered-install fixes, and, in pacquet 12.9.1, the two
[<since>] selector fixes. Order pacquet 12.9.1 so the `@pnpm/wasm` move,
which WebContainer users must act on, comes first.

Normalize issue links to the [#N] form and link the ETag fix to
pnpm/pnpm#16506.

Every fix was checked against the code released in 11.28.3 and
pacquet 12.9.0; none fixes a defect that never shipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(release): describe the frozen-lockfile fixes accurately in the 11.28.4 lead

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 22:13:02 +02:00
88bf5e4886 perf: watch every script's process group with one watchdog (#16490)
Without a controlling terminal, and in a concurrent recursive run of
pnpm v12, each script pnpm starts leads a process group of its own.
Since pnpm/pnpm#15564 a `sh` stood watch over each such group so that
it dies with pnpm. That doubled the processes a recursive run starts:
`pnpm -r run` over 50 projects started 100 shells, and on macOS each
bare `sh` spawn also walked PATH.

One watchdog per pnpm process now watches every group. It starts with
the first group pnpm creates. pnpm writes `+<pgid>` to its stdin when
it starts watching a group and `-<pgid>` once it is done with it. End
of input means pnpm died, and the watchdog kills every group still
listed, so the guarantee of pnpm/pnpm#15564 holds for any number of
concurrent groups. The watchdog keeps the groups in one string, so a
release costs two pattern expansions rather than a loop over every
group. A watchdog that died is replaced and takes over the groups
still watched: pacquet on its next write, pnpm v11 as soon as it exits.

Both runners change: pacquet's GroupWatch behind spawn_child, and
watchProcessGroup in pnpm v11's npm-lifecycle, whose watchdog and pipe
are unref'd so they never keep pnpm alive.

Closes pnpm/pnpm#16489

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:12:43 +02:00
de9611cf28 chore(release): 11.28.3 (#16444)
* chore(release): 11.28.3

* docs(changelog): curate the 11.28.3 release notes

Merge the eleven entries about names such as `constructor` into one,
group the patch changes by what the reader was doing, and add a lead
paragraph.

Drop the zlib-rs entry: that change only touched Cargo.toml, and the
TypeScript CLI does not use the Rust engine. It belongs to the next
pacquet release.

Rewrite the store index entry for CLI users. `ReadOnlyStoreIndex` and
`ImmutableStoreIndex` are library API documented in the
`@pnpm/store.index` changelog.

Say what the store prune mark-phase fix changes for the user.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:56:44 +02:00
Zoltan Kochan 4edeef9176 style(ts): port the perfectionist lint rules to ESLint (#16415)
Add TypeScript ports of the perfectionist rules that the Rust workspace
enforces through dylint.toml to @pnpm/eslint-config, with the same limits,
and fix every finding that needs no refactor.

The size and shape rules (function length, cognitive complexity, nesting,
local bindings, file length) are enforced for packages that already meet
them. The packages that do not are listed in PENDING_SIZE_AND_SHAPE_REFACTOR
in eslint.config.mjs and are refactored in follow-up changes.

The custom rules mirror perfectionist's semantics rather than reusing
sonarjs or max-lines-per-function, so that one limit means the same thing
in both codebases: a closure is a level of the body that contains it, a
nested function declaration is measured on its own, and a test-framework
callback plays the role of a #[test] function.

Bugs found and fixed along the way:
- a registry error body cut at exactly 64 KiB lost its truncation marker
- removing a dependency could leave dangling .bin shims
- constructor/__proto__ names crashed the projects graph, were skipped by
  the injected-deps syncer, and were dropped from writeWantedLockfile's
  returned lockfile

The CI chunk runner learns to run the node:test suite of the ESLint
config package.
2026-09-30 15:10:08 +02:00
Gabriel Mari S. FloresandCursor Agent a04402ff60 fix(npm-lifecycle): keep waiting for a zombie whose other threads still run (#16392)
Linux reports state Z both for a real zombie and for a process whose
main thread has exited while other threads keep running. The group wait
treated every Z as done, so pnpm exited while those threads were still
running. A Z member now counts as live when its task directory lists
more than one thread. A task directory that cannot be read does not.

refs pnpm/tasks#56

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-09-30 00:37:02 +02:00
9ce8c2e734 chore(release): 11.28.1, pacquet 12.8.0, pnpr 0.1.0-alpha.14 (#16312)
* chore(release): 11.28.1, pacquet 12.8.0, pnpr 0.1.0-alpha.14

* docs(release): park Rust changelogs and curate release notes

The release bump ran with pnpm 12.7.0, which since pnpm/pnpm#14934
writes the changelog of every private project to a committed
CHANGELOG.md. pacquet, `@pnpm/napi` and `@pnpm/pnpr` are private
in-repo but published, and release.yml and the publish-time changelog
composer both read their parked section. Move the three sections back
to .changeset/changelogs so the GitHub release bodies and the npm
tarball changelogs are not empty. The tooling follow-up is recorded
on pnpm/tasks#38.

Curate the pnpm 11.28.1, pnpm 12.8.0 and pnpr 0.1.0-alpha.14 pages:
add lead paragraphs, group patch entries by subject, merge entries
describing one change, drop internals no CLI user sees, and fix
"instead of" tails, implementation names and link placement. This
addresses both Qodo findings on the catalog entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 08:57:31 +02:00
Zoltan Kochan 266cbd9615 fix(run): pass script arguments through cmd unchanged on Windows (#16262)
`pnpm run` appended the arguments after the script name with JSON string
quoting when cmd parsed the script. cmd then expanded `%VAR%`, and the
target program's C runtime received doubled backslashes and `\t` for a tab.

Both CLIs now quote each argument the way npm does: quoted for the C
runtime, then with every cmd metacharacter escaped with `^`. When the
script's first word resolves to a .cmd or .bat file on the script's PATH,
such as a node_modules/.bin shim, the arguments are escaped a second time
because the batch file parses them again. The command that receives the
arguments is the last one of a chain, and the lookup searches the script
directory before PATH, as cmd does. Line breaks, which cmd cannot carry,
are still passed as the two characters `\n` / `\r`.

pnpm prints the script with its arguments quoted the POSIX way on every
platform, so the `^` escapes stay out of the `$` line, the lifecycle log
event and error messages. npm_lifecycle_script keeps the command that
runs.

Closes pnpm/pnpm#16257
2026-09-27 13:57:54 +02:00
16f56b91be fix: honor scriptShell when shellEmulator is set (#15865)
A configured scriptShell is spawned for pnpm run and lifecycle scripts even when shellEmulator is also enabled. The built-in emulator does not implement bash export or relative script paths the way Git Bash does, and the reported config sets both options, so the emulator was hiding the shell the user configured.

shellEmulator still selects the built-in shell when scriptShell is unset. A blank scriptShell is unset on both runners, so the fallback shell and the extra-argument quoting match. The shell is still selected first, so a .bat or .cmd scriptShell is rejected before anything runs.

Extra arguments are JSON-quoted only when cmd will parse the script (windowsVerbatimArguments). A non-cmd scriptShell, including a path such as C:\Program Files\Git\bin\bash.exe, gets POSIX quoting and is spawned without verbatim arguments, so the path stays one argument.

PATH, bin lookup, and auth are not changed.

Closes pnpm/pnpm#14719

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 11:23:16 +02:00
79c2bfc352 fix: exit with the script's status after Ctrl+C (#15854)
dash holds a terminal SIGINT until the foreground command exits, then
dies from that signal even when the command already returned a status.
`pnpm run` treated that shell death as a lifecycle failure and lost the
script's code.

A Bourne shell now runs the script behind a trap that re-raises SIGINT
when the command itself died from it (status 130) and otherwise lets the
script continue, as bash does. A deliberate exit of 130 is
indistinguishable from that report, so it is re-raised too. A second
interrupt always re-raises, so a loop of builtins can still be stopped.

Closes pnpm/pnpm#9945

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 15:40:46 +02:00
Altay c36eb58c77 fix(lifecycle): stop reading script output a background process holds open (#15638)
A lifecycle script that starts a process in the background and exits
leaves that process holding the script's stdout and stderr pipes. pnpm
waited for end of output on those pipes, so an install hung for as long
as the background process lived (an esbuild service started by a
`prepare` build, for example).

Both stacks now give the pumps one second after the script exits to
reach the end of its output, then stop reading. v12 closes a gate that
drops anything read later, and v11 destroys the child's output streams.

Closes pnpm/pnpm#5730
2026-09-25 23:04:13 +02:00
b341f5ada2 chore(release): 11.28.0, pacquet 12.7.0, pnpr 0.1.0-alpha.13 (#15647)
* chore(release): 11.28.0, pacquet 12.7.0, pnpr 0.1.0-alpha.13

* docs(release): curate the 11.28.0 and 12.7.0 release pages

Group both pages under subject headings with a lead paragraph that
names the headline changes and the security fixes, and move the
security entries to the top of the patch section.

Merge entries that describe one change: the node shim version files,
ranged selectors without a package.json, SemVer build metadata,
completion injection, custom modulesDir command lookup,
list --only-projects, store status, npm_execpath, catalog and npm-alias
workspace edges, the macOS CA fallback, and the prepare/devPreinstall
skips.

Drop entries that describe no change a published version could show:
the test-only --no-optional entry on both pages, the exported
CreateNewStoreControllerOptions type, the publish availability-probe
fix for a flag first shipping in 12.7.0, and the executable-bit check
the Rust CLI never had wrong.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(release): describe the bin shim part of the modulesDir entry accurately

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 12:01:04 +02:00
e8cfd554dc fix: pass /d /s /c to a cmd.exe scriptShell on Windows (#15509)
A scriptShell pointing at cmd.exe was spawned with `-c`, like any custom
shell. cmd.exe ignores `-c` and takes the first `/c` on its command line as
its switch, so a script such as `node install/can-compile` ran `an-compile`.

When the configured scriptShell's basename is `cmd` or `cmd.exe` on Windows,
pass `/d /s /c` and the verbatim script, as the default ComSpec path already
does. Applied to both the v11 lifecycle runner and the v12 executor.

Closes pnpm/pnpm#7181

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:15:18 +02:00
Altay 363129c613 fix(run): name the signal that killed a script (#15424)
A script killed by a signal has no exit code, so both CLIs reported
only "Command failed." pnpm v12 now reads the signal from the exit
status and prints "Command failed with signal SIGKILL." pnpm v11 keeps
the signal on the lifecycle error and the default reporter prints the
same title. The signal title also replaces the fixed "Test failed."
title of the test stage.

pnpm still re-raises the signal on itself afterwards, so the shell
keeps seeing an interrupted command. With v11, a signal that ends the
Node.js process on delivery (SIGKILL, SIGTERM) still ends pnpm before
the reporter prints.

Closes pnpm/pnpm#9821
2026-09-25 08:15:36 +02:00
AltayandZoltan Kochan c93eb93519 fix(lifecycle): name a scriptShell that does not exist in the spawn error (#15523)
A lifecycle or run script whose configured scriptShell cannot be found
failed with only an exit code (-4058 on Windows, -2 on POSIX) in pnpm v11
and with the package directory plus the OS error in pnpm v12, never naming
the shell.

When the spawn fails with ENOENT/NotFound, a scriptShell is configured and
the script's working directory exists, both stacks now report
ERR_PNPM_SCRIPT_SHELL_NOT_FOUND with the configured path. A missing working
directory produces the same OS error and keeps the existing report.

Closes pnpm/pnpm#7562

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-09-24 22:57:31 +02:00
Zoltan Kochan 4abfa5a66c fix(executor): end a script's process group when pnpm dies (#15564)
Without a controlling terminal each script runs in a process group of
its own, so that a relayed signal reaches it behind a shell that stays
its parent. That group is out of reach of whoever signals pnpm's own:
a SIGKILL aimed at pnpm's group, which is how Playwright's webServer
stops the command it started, ended pnpm and left the script running,
holding the caller's pipes open until the job timed out.

SIGKILL cannot be relayed, so a `sh` in a group of its own now watches
every process group pnpm creates. It reads a pipe only pnpm holds. pnpm
writes a line once it is done with the group, after the child and, for
a relayed signal, after the whole group has ended; an end of input
without one means pnpm died first, and the watchdog kills the group, as
the signal would have had the child stayed in pnpm's group. A normal
exit still leaves whatever the script started and left behind running.

Both runners gain the watchdog: pacquet in spawn_child, released from
SpawnedChild::wait, and pnpm v11 in relaySignals, released in settle.
`kill -9 -<pgid>` is the one spelling dash, bash, zsh and busybox sh
all accept.

Closes pnpm/pnpm#15555
2026-09-24 17:58:33 +02:00
Zoltan Kochan 3c217a9deb fix(exec): re-invoke an entry script only when it belongs to the running pnpm (#15408)
`resolvePnpmSelfCommand` decided whether `process.argv[1]` was pnpm from
its basename and the manifest above it, rejecting a script only when
another package claimed it as a bin. A script that no package claims, a
project's own `scripts/pnpm.mjs` among them, still passed.

pnpm's own code knows where it lives. The entry script is now accepted
only when its real path is the running module itself, as when a bundle
is run directly or copied elsewhere, or lies in the same package as that
module, as with `bin/pnpm.mjs` loading `dist/pnpm.mjs`. The nearest
`package.json` marks the package; the published `pnpm` and `@pnpm/exe`
tarballs carry only the one at their root. The basename set stays to keep
out hosts such as Jest, and a `pnpx` entry maps to the `pnpm.mjs` beside
it, since `pnpx` prepends `dlx` to `process.argv`. An entry that cannot
be resolved now
falls back to the pnpm on PATH, which is safe because the bundle-copy
case no longer depends on failing open.

The resolver moves to `@pnpm/cli.meta`, next to
`detectIfCurrentPkgIsExecutable`, so `npm_execpath` can share it without
`@pnpm/exec.npm-lifecycle` depending on the CLI runner. `npm_execpath`
previously took `process.argv[1]` unchecked, so a script run under
`pnpx` got `pnpx.mjs` and `$npm_execpath install` became `dlx install`.
It now gets the running pnpm's entry or executable, or the pnpm on
pnpm's own PATH when there is none, matching the runner's fallback. The `process.pkg` branch it
replaces was dead since `@pnpm/exe` moved to a Node.js SEA.

The lookup is cached for the process, since lifecycle scripts ask once
per script and neither `process.argv[1]` nor the module moves.

On Windows, `@pnpm/exe` installs `pnpx.exe` and `pnx.exe` as hardlinks
of the pnpm binary, which runs as `pnpm dlx` when launched through
either name. Re-invoking the current executable under one of them
prepended `dlx` again. Both versions now re-invoke the `pnpm` executable
beside the alias: v11 in `resolvePnpmSelfCommand` and `npm_execpath`,
and v12 through `pnpm_executor::current_pnpm_exe()` in `npm_execpath`,
`doctor`, the `verifyDepsBeforeRun` install, the pipeline agent and the
git-dependency build.

The `npm_execpath` fallback is resolved to an absolute path before a
script's PATH puts `node_modules/.bin` first, so a dependency's `pnpm`
bin cannot stand in for it.
2026-09-23 21:50:35 +02:00
Altay c699fbaf21 fix(run): stop nested scripts from duplicating PATH entries (#15307)
A script that runs `pnpm run` inherits the PATH its parent script got,
and extendPath prepended the same node_modules/.bin, node-gyp-bin, and
extra bin entries again on top of it, so PATH grew by one copy of them
per nesting level.

extendPath now drops inherited PATH entries that exactly match one of
the entries it prepends. The prepended entries keep their order and
priority, and every other inherited entry, including duplicates the
user put there, stays in place. The fix is applied to both the
TypeScript and the Rust implementation.

Closes pnpm/pnpm#5352
2026-09-23 01:06:59 +02:00
AltayandZoltan Kochan 30e3a83ed4 fix(exec): set package-manager environment variables (#15282)
Share lifecycle package-manager environment construction with exec and dlx
in both implementations. Child tools receive the invoking pnpm path, Node
path and initial working directory, matching npm exec.

Closes pnpm/pnpm#7037.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-09-22 23:05:23 +02:00
Efe Baran DurmazandZoltan Kochan 6b2a1056d4 fix: honor modulesDir for commands and CommonJS plugins (#15211)
Include the custom project modules directory in command shim NODE_PATH.
For symlinked tools, prepare each project's environment when pnpm runs
run/exec, install lifecycle scripts, and version hooks. Preserve symlinks,
project dependency isolation,
default layouts, and extendNodePath false.

Use the configured bin directory for bin/run/exec and install/version
hooks, including packageConfigs with separate lockfiles. Compare both
canonical paths when deciding whether a modules directory is the default.
Include development preinstall hooks and pnpr materialization. Reject
custom NODE_PATH entries containing path-list separators and parse
package configuration once per command.

Related to pnpm/pnpm#3604. ESM resolution remains outside this change.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-09-22 07:25:27 +02:00
Zoltan Kochan 0c1aed71e3 fix(run): suppress recursive failures after Ctrl+C (#15209)
Concurrent lifecycle scripts each raised their death signal on pnpm as soon as they exited. The first synthetic SIGINT reached the remaining scripts' relay handlers like a second Ctrl-C, escalated them to SIGTERM, and let recursive run report ordinary lifecycle failures before pnpm exited.

Group overlapping signal relays and raise a child's death signal once, after the whole group has settled. Delay lifecycle error reporting until that signal has had a chance to end pnpm.
2026-09-21 15:31:14 +02:00
Zoltan Kochan c4f877990c fix(exec): wait for the command after Ctrl+C and relay signals to it (#15121)
pnpm exec and pnpm dlx spawned the command through execa and handled
no signal themselves. A Ctrl+C ended pnpm at once, and execa's exit
cleanup then sent the command a SIGTERM while it was still handling
the terminal's SIGINT, so it never finished shutting down. A signal
sent to pnpm alone never reached the command at all.

The signal relay of the lifecycle runner is now a shared module of
`@pnpm/exec.npm-lifecycle`, and the tracked execa wrapper uses it: the
command gets a process group of its own when pnpm has no terminal,
pnpm relays SIGTERM and a SIGINT it received alone, escalates a second
interrupt to SIGTERM, and waits for the command and its group before
going on. The runner's own relay is unchanged in behavior. Terminating a
still-running child when pnpm exits is an option of the relay: the runner
always takes it, exec and dlx only for a detached command, since execa's
own cleanup terminates an attached one.

A recursive run that a signal reaches stops dispatching: queued
commands never start, a command the signal cut short is not journaled
as passed, and once every running command has settled pnpm re-raises
the signal on itself, so the shell sees an interrupted run. A single
command's exit status stays pnpm's.

Related to pnpm/pnpm#7374
2026-09-19 19:52:04 +02:00
Zoltan KochanandClaude Fable 5.1 2af480acf4 fix: signal the script's process group when pnpm has no terminal (#15119)
* fix: signal the script's process group when pnpm has no terminal

A signal aimed at pnpm alone is the only kind that reaches it without
a controlling terminal, and both runners relayed it to the shell that
runs the script. A shell that stays the script's parent, as dash does
for a command like `node server.js`, dies from SIGTERM at once and
holds a SIGINT until its child exits, so the script never heard that
it should stop, and a container runtime then killed it outright.

Without a controlling terminal each script now gets a process group of
its own, pnpm relays its signals to that group, and after a relayed
signal it waits for the group to empty before ending, so the script's
shutdown completes even though its shell is gone. With a terminal
nothing changes: the child stays in the foreground group, where the
terminal's own signals reach it and it may read the terminal.

pnpm v12 decides this in spawn_child and waits in SpawnedChild::wait,
which the output pump now goes through too; members that became pnpm's
children, as they do when pnpm is PID 1, are reaped along the way.
pnpm v11 spawns the shell detached and polls ps for live members of
the group.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(cli): decide the exec process-group test by the terminal it has

The process-group check ran under whatever terminal the harness had,
so it held on a developer's terminal and failed on CI, which has none.
It now runs pacquet on a pseudo-terminal for the foreground case and
in a session without one for the own-group case. The terminal helpers
of the interrupt tests move to the suite's utilities for that.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(cli): run the remaining foreground-group checks on a terminal

The filtered-run and serialized-task process-group checks made the
same assumption as the exec one: that the harness holds a terminal.
They now run pacquet on a pseudo-terminal too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(executor): assert the foreground group only at a terminal

The unit test assumed the test process holds a terminal. It now
checks the contract for whichever case it runs in: a shared group at a
terminal, the child's own group without one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(npm-lifecycle): probe the process group without ps, and raise the death signal after the wait

The group wait asked ps and took any failure for an empty group, so a
container without a usable ps cut the shutdown short. The group is
now probed the way the kernel counts it, with a signal of 0, and on
Linux a member that exited but is not reaped yet, as happens when pnpm
is a container's PID 1, is told apart through /proc.

A shell killed by the relayed signal made the runner raise that
signal on pnpm at once, before the group wait, and pnpm's own listener
was already consumed, so pnpm died while the script was still shutting
down. The raise now comes after the wait. An onSpawn observer that
throws signals the whole group too.

The tests observe the runner's exit rather than its output: a script
that outlives the runner keeps the shared pipe open, which hid this.
A failed setsid in the test helper fails the spawn.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(npm-lifecycle): give up the group wait when the group cannot be observed

A probe error other than "no such process", such as a permission
error from a member that changed user, made the group count as live on
every poll, so the wait never ended. Such an error now rejects the
wait and the runner goes on. A /proc entry that vanished between the
listing and the read has exited; one whose state cannot be read counts
as live.

The runner's doc says that a script's death signal, raised after the
wait, ends pnpm before the callback unless something handles it. The
tests kill the detached process group whatever way they end.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(npm-lifecycle): leave unreadable /proc entries out of the group

An entry whose state cannot be read is not a member pnpm can observe:
it exited since the listing, or it belongs to another user under a
restricted process table. Counting it as live kept the wait going on
every poll.

The test helper kills the detached group as soon as the runner has
exited, before waiting for the output pipe a survivor would hold open,
and its timeout kills the group rather than the runner alone. The
cleanup helper is shared through `@pnpm/prepare` and accepts only a
group that is gone already. Two narrating comments in the Rust tests
are gone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(npm-lifecycle): cover the process-table scan with a table of its own

The scan takes the process table's path, so a test can supply one with
a zombie, a live member and an entry whose state cannot be read, and
show that the wait ends for the first, goes on for the second, and does
not mistake the third for a member.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(npm-lifecycle): helpers after their use, and a bump for the shared cleanup helper

The process-table test's helpers follow the tests that call them, and
the one that writes the table states what it creates and returns. The
changeset lists `@pnpm/prepare`, which gained the shared cleanup helper.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(npm-lifecycle): keep only the non-obvious part of the table helper's doc

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(npm-lifecycle): clear the losing deadline timer

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 14:48:10 +00:00
Zoltan Kochan 4eb09c84e5 refactor: move npm-lifecycle into the pnpm monorepo (#15113)
Import the lifecycle runner from pnpm/npm-lifecycle at 178f3d5 as
@pnpm/exec.npm-lifecycle, so it is maintained alongside its consumer
and a fix no longer needs a separate release.

The source is converted to TypeScript with typed options, package
data, log and child-process interfaces, which replaces the any-typed
declaration in __typings__/local.d.ts. The legacy pkg._data unwrapping
and the unused opts.config are dropped. The tests are ported to jest.

The node-gyp wrappers ship with the package; the runner finds them
beside lib in the package and in dist once the CLI bundle has
flattened the module, where the CLI ships its own copy. The manifest
updater keeps node-gyp-bin in the package's files. @yarnpkg/shell's
declarations import micromatch's, so a packageExtensions entry gives
it @types/micromatch. The Artistic-2.0 license is preserved.

The imported code includes the fix for the terminal SIGINT being
relayed to a script that already had it (pnpm/npm-lifecycle#62), which
this brings to pnpm v11; a pseudo-terminal test covers it in the
package and in the CLI.

Related to pnpm/pnpm#7374
2026-09-19 13:01:50 +02:00