Decide whether `node_modules` is up to date in one place for both `pnpm install` (`optimisticRepeatInstall`) and `verifyDepsBeforeRun`.
- In Rust (`pnpm-package-manager`), combine the static workspace-state and filesystem checks with the content freshness checks into `freshness::check_freshness`, parameterized only by `GatePolicy`, so `check_optimistic_repeat_install_with_state` and `check_deps_status_before_run` call the same decider.
- Share `gvs_build_markers_complete` and `check_strict_dep_builds_for_fast_path` between `install_already_up_to_date` and `repeat_install_verdict`, and share `recorded_workspace_state` with repeat-install test fixtures.
- Fix two Rust gate divergences uncovered by unifying `check_freshness`: `Gate::Install` now rejects drifted `configDependencies`, and `Gate::Run` now rejects a missing branch lockfile when `useGitBranchLockfile` is enabled instead of letting `node_modules/.pnpm/lock.yaml` stand in for it.
- In TypeScript (`@pnpm/deps.status` and `@pnpm/exec.commands`), check `catalogResolutionsAreUpToDate` in `assertWantedLockfileUpToDate`, avoid mutating `config.ignoredOptionalDependencies` in place, share `recordValidatedWorkspaceState` between root-project and workspace checks, respect `opts.lockfileDir` in `checkRootProjectDepsStatus`, and warn when a `verifyDepsBeforeRun` install leaves `node_modules` out of date.
Closes https://github.com/pnpm/tasks/issues/142
Generated with Antigravity (Claude Opus 4.7)
Unify repeat-install freshness and `verifyDepsBeforeRun` validation across
pnpm v11 and v12 (Closespnpm/tasks#161, pnpm/tasks#157, pnpm/tasks#148):
1. Centralize v11 workspace state updates in `@pnpm/workspace.state`
(`preserveCarriedSettings`) and `@pnpm/installing.commands`
(`workspaceStateUpdate.ts`), preserving carried `dev`/`optional`/`production`
settings when `ignoredWorkspaceStateSettings` ignores them and preserving
v12-recorded `autoDedupe`. A filtered install that prunes direct
dependencies no longer records the workspace root as installed.
2. Hold `{.}...` workspace dependencies for non-recursive `pnpm run`,
`pnpm exec`, and `pnpm restart` inside a workspace project in v11
(`selectProjectsToVerify.ts`).
3. Align v11 recorded validation (`checkRecordedWorkspace`) and legacy
unrecorded validation (`checkModifiedSinceLastValidation`) in
`@pnpm/deps.status` with v12's `check_freshness`, including empty-project
handling, `dedupeDirectDeps`, `dedupePeerDependents`, `autoInstallPeers`,
`excludeLinksFromLockfile`, catalog/override config comparison, and
`opts.lockfileDir` resolution.
4. Forward effective CLI flags (`--lockfile-dir`, `--modules-dir`,
`--virtual-store-dir`, `--store-dir`, `--dir`, `--workspace-root`, `--prod`,
`--dev`, `--no-optional`, `--filter`, `--filter-prod`, `--test-pattern`,
`--changed-files-ignore-pattern`, and `--config.*`) when
`verifyDepsBeforeRun: install` spawns `pnpm install` in both v11 and v12.
5. Align `GatePolicy::dedupe_peers` and empty-project predicates in v12
`pnpm-package-manager`, and run `repeat_install_oracle.rs` across v12, v11,
and mixed v11/v12 writers in CI, with a longer nextest period for the
peer convergence harness that shares the job.
6. Parse `--config.<key>=<value>` with the type the command declares for
`<key>` in v11, so the list and number options forwarded to the spawned
install arrive as lists and numbers; forward `--npmrc-auth-file` in v11
and `--test-pattern` / `--changed-files-ignore-pattern` in v12, so both
forward the same set; and skip the current-lockfile read for a project
whose manifest changed but whose lockfile did not, as v12 does.
7. Resolve relative v11 store overrides against the workspace or project
root before forwarding them to the spawned install. Remove the isolated
linker's duplicate package merge so filtered installs retain unselected
dependencies and drop obsolete selected dependencies from the current
lockfile. Cover both cases with CLI regressions. The restore API also
handles a selected project absent from the previous current lockfile.
8. Record workspace validation from the projects the installers actually
processed. Ignored projects receive no new proof, and empty or entirely
ignored selections leave state untouched. Cover both lockfile modes and
an ignored workspace root.
Running `pnpm dlx --package=<pkg>` with no command installed the package
and then crashed trying to run an empty command. The handler now throws
ERR_PNPM_DLX_MISSING_COMMAND before doing any work, matching the v12
CLI MissingCommand error for the same invocation. Only v11 is affected;
v12 already rejects this case.
---------
Co-authored-by: Dextheking1 <Dextheking1@users.noreply.github.com>
The install that verifyDepsBeforeRun spawns before `pnpm run` and `pnpm exec`
now receives the parent's `--config.<key>=<value>` options. Settings from
config files and pnpm_config_* environment variables already reached the
child; command-line ones did not, so e.g. `--config.lockfile-dir` was lost and
the install wrote its lockfile into the project.
Both CLIs forward the options as given, at the single place that spawns the
install, so the recheck after waiting for another gate's install gets them too.
The confirmation prompt shown for `verifyDepsBeforeRun: prompt` omits them,
since a `--config.//registry/:_authToken=...` value would otherwise be printed.
pnpm v11 does not forward `--config.dir`: the install already starts in the
resolved directory, where a relative value would resolve a second time.
pnpm v11 carries the options on the config context (`rawCliConfig`) rather than
on cliOptions, which would make them a setting and an explicitly set key.
Closes https://github.com/pnpm/tasks/issues/60
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
When verifyDepsBeforeRun is `install` (the default) and node_modules are
out of date, `pnpm run` and `pnpm exec` start `pnpm install` first. In a
sandbox with a read-only store or no network that install cannot succeed,
so no script could run at all.
A failed install now prints a warning that node_modules may be out of sync
and the command continues, the same outcome as the `warn` action. The
install has already printed its own error. The warning is fixed text, so it
echoes no filter selectors, and it names `verifyDepsBeforeRun: false` as the
way to skip the install.
An install ended by a signal, or by Ctrl+C on Windows (exit code
0xC000013A), still stops the command, so an interrupt is never turned into
a script run. The `prompt` action shares the install path, so a confirmed
prompt behaves the same way.
Implemented in both the TypeScript CLI and pacquet, with the settings docs
for both versions updated.
Related to https://github.com/orgs/pnpm/discussions/16212
A filtered install leaves the workspace dependencies of the projects it
selected without a modules directory, but the install the
verifyDepsBeforeRun gate spawns for a filtered run or exec selects them
(each selector gets "..."). The status check only looked at the projects
the command selected, so it reported the state up to date and the
dependency stayed uninstalled.
Both stacks now verify the selected projects plus their transitive
workspace dependencies, computed at the gate from the graphs the
selection was drawn from. A --filter-prod-only project walks the
prod-pruned graph. When a negated selector is present, the set is
intersected with the projects the spawned install selects, because
negated selectors reach that install unchanged; otherwise the gate would
require an excluded project and install on every run. The v12 helper
returns early when verification is disabled.
In v11 the computation lives in exec.commands (selectProjectsToVerify)
and is passed to checkDepsStatus as selectedProjectsGraph, leaving
deps.status unchanged. In v12 the graph walks the recursive commands
share move to cli_args/recursive/dependencies.rs.
Refs pnpm/tasks#45.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.
The verify-deps-before-run gate spawned an install whenever no workspace
state file existed, even for projects that give an install nothing to
do. That install only wrote node_modules and pnpm-lock.yaml, which
became visible in directories the workspace packages patterns leave
out after pnpm/pnpm#15125.
The gate now returns up to date when no state file exists and no
project the install would cover declares a dependency, a required peer
that autoInstallPeers would fetch, or an install lifecycle script
(preinstall through postprepare, or an implicit binding.gyp build) that
ignoreScripts does not disable. pnpm:devPreinstall counts only in the
workspace root, the only place an install runs it. Under a shared
lockfile every workspace project is checked, and the projects are walked
only when the root manifest itself has nothing to install. With separate
lockfiles a non-recursive run checks only its own project. A
non-recursive pnpm 11 command gets no allProjects, so the gate reads the
workspace itself, as checkDepsStatus does.
A readPackage hook can add dependencies, so any pnpmfile the install
would load (looked up from the lockfile directory) keeps the install.
A workspace that cannot be read, or a lockfileDir pinned away from the
project, also keeps it. Projects with lifecycle scripts still
install on the first run, so those scripts keep running on first use.
Closespnpm/pnpm#16313
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The verify-deps-before-run gate spawns `pnpm install` when node_modules
are out of sync. When the root package.json still keeps overrides,
packageExtensions, patchedDependencies or ignoredOptionalDependencies in
its `pnpm` field, which pnpm no longer reads, the lockfile records
settings the effective config can never reproduce. Every `pnpm run` then
found the tree stale and rewrote the lockfile without them, silently
dropping security overrides.
The gate now refuses that install (in prompt mode, once the user
confirms it) and fails with ERR_PNPM_VERIFY_DEPS_BEFORE_RUN and asks to move the settings to
pnpm-workspace.yaml. Migrated keys the lockfile does not record keep the
existing behavior. Fixed in both the Rust and the TypeScript CLI.
Closespnpm/pnpm#16278
`pnpm --filter <selector> run|exec` checks the workspace state before it runs
the command and spawns an install when that state is outdated. The spawned
install carried no project selection, so it covered every project of the
workspace instead of the ones the command was limited to. And a state that
records a filtered install exempted every project from the modules-directory
requirement, so the gate answered "already up to date" for a project the
earlier filtered install had not materialized and ran the command without
installing it.
Forward the `--filter` and `--filter-prod` selectors to the install the gate
spawns, each extended with `...` so the workspace projects a selected project
depends on are installed too, and hold the projects the gated command selected to the
modules-directory requirement, in both the TypeScript and the Rust CLI.
Closespnpm/pnpm#11865.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With `tasks` declared, a recursive run with a RegExp selector now seeds
one task per script the selector matches in each project, so each
resolves the `dependsOn` declared for its own name, as a run of that
script by name does. The selector string itself matched no `tasks`
entry, so its task fell back to `^<selector>` and the matched scripts'
declared dependencies never ran.
Resolving the union of the matched scripts' entries onto one
per-project task was rejected. That task runs its scripts concurrently,
so a matched `test` could not wait for a matched `build`. A `^build`
entry also pulled in a `build` task that the dependency project's
selector task ran again.
A project the selector matches nothing in keeps a requested
pass-through task under the selector name, with no dependencies.
Without `tasks`, or with an exact `tasks` entry under the selector
string, the selector stays a single task per project.
The hidden-script check runs per project over the requested tasks. A
selector that matches only hidden scripts in a project still fails, but
a hidden script alongside visible ones is filtered as before. A
requested task that a `dependsOn` declaration targets is exempt, like
any `dependsOn` reference. The exempt set is every dependency of a task
with a `tasks` entry, since a task without one only has the default
`^<name>` edge. It is read from the full graph before sequencing, in
declaration direction when `--reverse` inverted the edges.
`--resume-from` anchors on every task requested in the anchor project
when the invocation's own task is absent, which is the case for an
expanded selector. Summary keys qualify every task but the one
named by the invocation, so expanded tasks do not share a key.
Closespnpm/pnpm#15596
Co-authored-by: Dex <4175097+Dextheking1@users.noreply.github.com>
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Concurrent installs that resolve to the same global virtual store slot
ran its lifecycle scripts in the one shared directory at once. A script
that rewrites its own files, such as esbuild's install.js, then failed in
one install, and that install removed the slot the other installs had
already linked.
Builds into a global virtual store slot now hold a directory lock at
`<slot>/.pnpm-build.lock`, in both pnpm v11 and v12. A build that finds
the slot's `.pnpm-needs-build` marker gone once it gets the lock skips
the build, since another install finished it. A build marks the slot
"started" before it writes into it, and a waiter that finds that mark
skips the build too, since another install's build of it failed or died. An install also takes
the lock before it re-imports a slot that still carries the marker,
which would otherwise overwrite a build in progress, and so does a
side-effects cache hit that re-imports its overlay into the slot. A
patched package whose scripts --ignore-scripts withheld keeps its marker
until the scripts have run.
A failed build no longer removes the slot. It leaves the "started"
`.pnpm-needs-build` marker instead, which the next install already treats
as an interrupted build: it re-imports the pristine files and builds
again.
The TypeScript directory lock moves from `@pnpm/exec.commands` into a new
`@pnpm/fs.dir-lock` package so the build step can use it.
Closespnpm/pnpm#15568
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
In a workspace reached through a symlinked directory, `pnpm -r exec pwd`
printed the resolved physical path instead of the logical path
(Closespnpm/pnpm#1550).
The spawn cwd already is the logical project path. What leaks the physical
path is the inherited PWD environment variable: it still points at pnpm's own
cwd, disagrees with the child's actual cwd, and shells that trust PWD fall
back to the physical working directory. Set PWD to the directory the command
runs in (unix only). pacquet's command_in_dir gets the same override so both
implementations agree.
External binaries that print the physical directory by default (e.g.
coreutils pwd without -L) are unaffected by any package manager and out of
scope.
verifyDepsBeforeRun spawned its install without a lock, so N concurrent
`pnpm run` or `pnpm exec` against a stale node_modules started N installs
racing in the same directory and failing with filesystem errors.
The gate now takes a per-workspace advisory lock around the install:
pacquet's DirLock in the per-user lock directory, keyed by the canonical
workspace root, and in the TypeScript CLI a mkdir lock now shared with the
task-run journal. A gate that finds the lock held re-checks the dependency
status once it holds the lock, and skips the install when the first gate's
install already brought the tree up to date. A gate that gets the lock
immediately does not re-check, so an uncontended run pays no extra check.
The wait is bounded at 5 minutes. A gate that times out, or cannot
establish the lock, installs without it as before.
The TypeScript mkdir lock also takes over a lock whose holder process on
this host has ended, matching the liveness takeover of pacquet's DirLock.
Closespnpm/pnpm#14551
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
`pnpm <command>` falls back to `pnpm exec <command>` when the project has
no script by that name, but `pnpm --filter <project> <command>` and
`pnpm -r <command>` went straight to the recursive script runner and
failed with ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT, so a bin from the selected
projects' dependencies could not be run through the shorthand.
When the shorthand is used and none of the selected projects has a
matching script, the recursive run now hands the command to a recursive
`exec` over the same selection, with the same recursive options. The
decision is made on the task graph as built, before the run-only resume
and sequencing, which do not apply to `exec`. If at least one selected
project has the script, the scripts run as before. `--if-present` and
`--dry-run` keep their meaning, `test` and `start` keep their
missing-script behavior, and an explicit `pnpm run` still reports the
missing script.
Closespnpm/pnpm#10151
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
The dlx cache key covered the packages, the registries, the allowed
builds and the architectures, but not the Node.js runtime. A command run
under a different Node.js major reused the cache entry installed under
the first one, including packages built by lifecycle scripts, so a
native addon failed to load with ERR_DLOPEN_FAILED.
Both stacks now add the engine name (`<platform>;<arch>;node<major>`)
of the Node.js that runs the builds to the key: `getSystemNodeVersion()`
in v11 and the `node` on PATH in v12. The `nodeVersion` setting is not
used, since it only sets the target of the engines check.
Closespnpm/pnpm#8611
`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.
`pnpm restart` ran the "stop" script and then failed with
"Missing script: restart" when the package had no "restart" script,
leaving the app stopped. The "restart" step is now skipped when the
script is missing, so the command falls back to "stop" then "start",
as npm does. Fixed in both the TypeScript and the Rust CLI.
A package that defines "restart" still runs "stop", "restart", and
"start". Dropping "stop" and "start" in that case, as npm does, would
change stable behavior.
Closespnpm/pnpm#4750
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.
Closespnpm/pnpm#7037.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
* fix(exec): find the project's bins when run from a subdirectory
`pnpm exec` and the `pnpm <command>` fallback keep the caller's cwd since
pnpm/pnpm#10445, but the only PATH entry pointing at the project,
`./node_modules/.bin`, is relative and resolved against that subdirectory,
so executables installed in the project were no longer found. The
project's `node_modules/.bin` is now added as an absolute entry when the
run directory differs from the project directory, and `PNPM_PACKAGE_NAME`,
`.pnp.cjs`, and the package map are read from the project instead of the
cwd. This matches the Rust CLI, whose `ExecDirs` already separates the run
directory from the project directory.
Closespnpm/pnpm#5068
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(exec): add the project's bin dir to PATH relative to the run dir
An absolute entry is rejected by makeEnv when the project path contains
the PATH delimiter, which the existing "directory with path delimiter"
test guards against for the relative `./node_modules/.bin` entry. The
subdirectory test now runs in such a project. The changeset states each
behavior in its own sentence.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(exec): cover the PnP lookup from a subdirectory
The subdirectory test now asserts that `.pnp.cjs` is found in the project,
not the cwd. The comment above the project-dir decision keeps only the
reasons the code cannot carry.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Reuse the install build-approval helper for global installs and dlx.
Keep a lockfile in each isolated dlx cache entry so approved packages
can be rebuilt before running the requested binary. Reload persisted
approval decisions into the rebuild configuration.
Recheck pending builds on cache hits in both v11 and v12. Refresh
caches that do not have the lockfile required for rebuilding in both stacks.
Cover noninteractive installs, approval and build artifacts, caller
settings isolation, and reuse of an approved cache entry.
Closespnpm/pnpm#14943.
Persist passed recursive tasks incrementally in a versioned journal under the workspace's node_modules directory. Hash the invocation's task graph, script commands, arguments, environment, and execution settings so only compatible state is reused.
Use unique per-run journals, cross-process serialization around the atomically published latest-run pointer, open file handles, and newline-committed records to reject stale, superseded, malformed, torn, or unsafe state. Remove each journal after its invocation succeeds and preserve the existing dependency-pruning fallback when no compatible state is available.
Implement the same state format and resume semantics in the TypeScript CLI and Rust pnpm port, with shared identity fixtures guarding cross-implementation compatibility.
Related to pnpm/pnpm#14211.
RegExp script selectors included matching names even when their script bodies
were empty, unlike exact-name selection. Recursive task orchestration could
therefore dispatch tasks pulled in through dependsOn before eventually
reporting that the requested script did not exist.
Filter empty bodies at the shared script-selection boundary in both the
TypeScript CLI and pacquet, so pre-dispatch validation observes the same
missing-script semantics for exact and RegExp selectors.
Related to pnpm/pnpm#14211.
Add an optional positive-integer concurrency limit to workspace task settings and carry it onto resolved task nodes.
Reserve permits in the dependency-ready scheduler rather than inside workers. This lets unrelated task names use available workspace capacity, while tasks waiting on their own limit remain undispatched and do not start after a bailed failure.
Implement matching configuration validation, scheduling behavior, and integration coverage in the TypeScript CLI and pacquet.
Implements the per-task concurrency extension from pnpm/pnpm#14211.
Move workspace task graph construction and scheduling out of the recursive command implementations into dedicated TypeScript and Rust packages.
The Rust crate depends on the lower-level dependency graph sequencer so package-manager code can consume it without creating a dependency cycle. Existing recursive run and exec consumers now import the shared implementations, preserving their current behavior while enabling the follow-up scheduler migrations tracked by pnpm/pnpm#14211.
Related to pnpm/pnpm#14211.
Replace the chunked topological scheduler of recursive run/exec with
per-task scheduling in both stacks: a task — a (project, script) pair —
becomes runnable when every task it depends on has completed
successfully, and runnable tasks are dispatched under the
workspace-concurrency limit with no barrier between
dependency-independent tasks.
A new "tasks" section in pnpm-workspace.yaml declares task dependencies
with the caret convention ("^build" = the task in each workspace
dependency, "build" = the task in the same project). A task with no
entry behaves as depending on its own name in the workspace
dependencies, which is exactly what chunking implied. A project without
the script becomes a pass-through node that is reported skipped and
keeps the chain intact.
Also per the RFC: task-graph cycles are ERR_PNPM_TASK_CYCLE naming the
participating tasks, scoped to the invocation's selected graph, with
ignoreWorkspaceCycles: true downgrading the error to a warning;
--resume-from excludes exactly the anchor's transitive dependencies;
--reverse runs the reverse graph; under --no-bail dependents of a
failed task are reported skipped and do not add to the exit code; with
--bail, the first failure ends the run at once and nothing new is
dispatched; output is inherited only when at most one script can ever
be in flight; and pnpm -r run --dry-run [--json] prints the resolved
task graph without running anything (the verify-deps check included).
Implementation: the projects sorter exposes the tunneled dependency-edge
map (filteredProjectsDependencies / filtered_projects_dependencies)
instead of only its flattened chunks; the recursive summary is
task-keyed (dependsOn-pulled tasks get "<dir>#<task>" keys); pacquet's
inert --sequential now means concurrency 1 and its --no-sort
resume/reverse handling is aligned with the TypeScript CLI; the dead
chunk helpers are removed.
Related to https://github.com/pnpm/rfcs/pull/23
The Rust CLI registers only `--prod` on `install`, so the `pnpm install …`
command the verify-deps-before-run gate reproduces from a production-only
install — built with pnpm's `--production` spelling — was rejected by the
argument parser, aborting every `pnpm run` (Closes pnpm/pnpm#14147).
`--production` is the setting name behind `--prod`, and the TypeScript CLI
accepts it on every command where `--prod` selects dependency groups, so
restore it as an alias there too (`audit`, `licenses` and `outdated` already
had it), and emit the documented `--prod` in the reproduction command in
both stacks.
Closes#14147
The TypeScript CLI accepts `--stream`, `--aggregate-output`,
`--use-stderr`, `--reporter-hide-prefix`, `--ignore-workspace`, and
`--workspace-packages`; pacquet rejected all six. Section 2 of
pnpm/pnpm#14101.
`--stream` is the substantial one. A recursive `run` inherited the
terminal for every project, so pacquet had no way to attribute a line to
the project that wrote it, and `--parallel` — which expands to
`--stream` in pnpm's `run` shorthand table — produced unreadable
interleaved output. `RunScript` grows a `ScriptOutput`: `Inherit` keeps
the old path, `Streamed` pipes the child and republishes each line as a
`pnpm:lifecycle` event through the new `StreamedScript`, which also
absorbs the line pumps `run_lifecycle_hook` already had. The reporter
gains `streamLifecycleOutput`, so the lifecycle stream renders
append-only while the rest of the frame still redraws in place — the
same split pnpm's reporter makes.
`--aggregate-output` buffers a script's events until it exits and then
renders the run as one block, formatting at flush time so the prefix
color wheel advances in print order. `--reporter-hide-prefix` drops the
prefix from the script's own output lines only, leaving the `$ <script>`
echo and the `Done` / `Failed` line labelled. It is a `run` / `exec`
option, so it is scope-validated and hidden like the other
command-scoped globals; a recursive `exec` reads its explicit `false` as
the signal to start prefixing, matching pnpm's
`reporterHidePrefix === false` gate.
`--ignore-workspace` stops the workspace search in `Config::current`, so
`pnpm-workspace.yaml` contributes neither settings nor sibling projects
and a blocked dependency build is not scaffolded into its `allowBuilds`.
`--workspace-packages` is resolved into
`Config::workspace_package_patterns` alongside the manifest's own
`packages`, which `discover_workspace_projects` now takes from the
config rather than re-reading the manifest at every call site.
The five boolean settings also become readable from
`pnpm-workspace.yaml`, the global `config.yaml`, and `PNPM_CONFIG_*`.
This is pacquet-only: the TypeScript CLI already has all six.
Registries do not all lay out tarball URLs the way the npm registry does.
JFrog Artifactory repeats the scope in a scoped package's tarball filename
(`@acme/widget/-/@acme/widget-1.0.0.tgz`) where npm strips it. pnpm cannot
rebuild such a URL, so it writes it out for every scoped package instead of
omitting it, and the lockfile carries a host-specific URL per dependency.
pnpm had no place to record such a fact, because it had no place to
describe a registry at all — only three ways to name one. `registries`
mapped a scope to a URL, `namedRegistries` mapped a bare-specifier prefix
to a URL, and neither could carry anything else.
`registries` now declares a registry once, keyed by its URL, with every
fact about it in the entry: a `serverType`, the `scopes` routed to it,
and the `prefix` it answers to. `serverType` has three states:
undeclared strict; only the exact canonical URL is reconstructible
npm also serves the percent-encoded scoped path
artifactory repeats the scope in the tarball filename
registry.npmjs.org resolves to `npm` as a built-in, so its behavior is
unchanged and the old hostname check becomes that one default rather than a
special case in the predicate. `npm` cannot be the default: asserting
npmjs-compatibility is a claim only the operator can make.
The URL is the key because every fact in an entry is a fact about that
server. Keying the layout by scope would bind it to whoever the scope
currently points at, so two developers whose scope resolves differently
would write lockfiles that disagree about which URLs may be omitted.
`scopes` and `prefix` are routes to the registry and are inverted at
config-read time into the two lookups the rest of pnpm already queries,
leaving the resolver, installer, and lockfile layers untouched and the
precedence chain (builtin < .npmrc < yaml < `_auth` < CLI) unchanged.
The layout is declared, never inferred. Sniffing the registry URL cannot work:
a virtual repository serves both layouts at once, depending on whether each
package was synced from upstream or published locally, so no registry-level
signal — route, response header, or probe — decides it. Declaring it also
keeps a wrong guess a fixable misconfiguration instead of a silent breakage.
`serverType` feeds a single URL builder that both sides of the lockfile use:
the writer omits a tarball URL only when the builder reproduces it, and the
reader rebuilds it with the same call. They therefore agree by construction,
so pnpm never has to assume a registry serves some second URL as well.
The setting lives in pnpm-workspace.yaml rather than .npmrc because the
lockfile depends on it: one developer omitting URLs that another reconstructs
differently would break a frozen install. A `serverType` in the global
config.yaml is ignored for the same reason, while the routes declared
alongside it are kept. Credentials are rejected there — the file is
committed — and still belong in .npmrc. The registry URL is the map key, so
the request-destination env gate applies to keys as well as values.
Credentials and unknown fields are refused after parsing, since a parse
error renders the offending source line verbatim.
A map whose values are all strings is the older `<scope>: <url>` shape and
is still read as one. Mixing the two shapes in one map is refused, and so is
a URL-keyed entry written as a string. `namedRegistries` is deprecated in
favor of `prefix` and is read only for prefixes `registries` does not
declare; a prefix stays singular because it is the registry's identity in a
lockfile dep path.
An entry that routes nothing to itself and matches no configured registry is
reported as a warning rather than silently ignored; it is a warning and not
an error because a shared config dependency can legitimately describe
registries a given project does not use.
Config dependencies and pnpr-server-mode resolution pass no server type; in
both, the writer and the reader share that default, so they stay consistent.
Closespnpm/get-npm-tarball-url#16. Supersedes pnpm/pnpm#13920.
Six independent pacquet-side defects found while installing and building
n8n with pnpm 12. Each is a divergence from the TypeScript CLI, so every
fix moves pacquet onto pnpm's existing behavior rather than inventing new
behavior.
Finding 2 — an `allowBuilds` placeholder written by pnpm made
`WorkspaceSettings` refuse to load the config at all. The value is now an
`AllowBuild` enum; only decided entries reach `Config::allow_builds`,
matching `createAllowBuildFunction`. The raw value stays on
`WorkspaceSettings` so `pnpm config list` and the `updateConfig` hook
still see what the file says.
Finding 3 — `diffy`'s matcher is byte-exact, while `@pnpm/patch-package`
compares lines with trailing whitespace stripped and retries a hunk
within twenty lines of its recorded position. `pacquet-patching` now
applies hunks itself with those tolerances, keeping `diffy` for parsing.
The file is modeled as `split('\n')` throughout, so untouched CRLF lines
keep their `\r` and a file without a final newline keeps that shape.
Finding 4 — `Config::user_agent` is threaded to install lifecycle
scripts, `pnpm run`, `exec`, and `dlx`, which previously saw nothing or
the bare string `pnpm`. It still reports `node/?` rather than the host
Node version; filling that in costs a `node --version` spawn on every
command, which is a separate trade-off to make.
Finding 5 — `/pattern/` script selectors select every matching script in
single-project and recursive runs, mirroring `tryBuildRegExpFromCommand`
+ `getSpecifiedScripts`. This adds `regex` to the workspace
dependencies; it was already in the lock transitively.
Finding 6 — concurrent `packageManager` switches raced on the shared
global-virtual-store slot: the destructive re-stage removed a directory
another process was still writing, and the native-binary relink used
unlink-then-hardlink, pulling the executable out from under a sibling
running it. The install is now serialized by an advisory lock (a new
`pacquet_fs::DirLock`, which gives up rather than failing so a lost lock
is never worse than today), and the relink skips an already-correct
destination and otherwise swaps via rename.
Finding 8 — settings drift under a frozen install reports
`ERR_PNPM_LOCKFILE_CONFIG_MISMATCH` naming the one field instead of
`ERR_PNPM_OUTDATED_LOCKFILE` with the whole map dumped; ignored build
scripts keep their `(patch_hash=…)` suffix; and a deprecated package is
reported once, since pacquet's resolver re-emits one it later meets at a
shallower depth.
All six are pacquet-only bugs; the TypeScript CLI already behaves this
way, so there is nothing to mirror.
Closespnpm/pnpm#13322
Reverts the lexicographical sorting of scripts introduced in pnpm/pnpm#13074, as this was a breaking change in behaviour. When using `pnpm run --sequential /regex/`, scripts will now be executed in the order they are listed in `package.json`, giving developers control over the order without needing odd script prefixes. Fixespnpm/pnpm#13174.
Restored `--sequential` (`-s`) CLI flag for `pnpm run` to force `workspaceConcurrency=1` for matched scripts across workspace packages and within individual packages. Added `.sort((a, b) => a.localeCompare(b))` to `getSpecifiedScripts` so that multi-script execution triggered by a RegExp selector executes in deterministic lexicographical order. Updated `cliOptionsTypes` in TypeScript and `RunArgs` across all Rust CLI commands (`run`, `clean`, `restart`, `stop`, `dispatch_script`) to ensure seamless parsing and parity.
Main thread panicked: begin > end (105 > 28) when slicing
`@pnpm/npm-lifecycle@1100.0.0(patch_hash=e3541c…)(supports-color@10.2.2)`
at pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:2490:51
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
The git-resolver unit tests hit live github.com by default: the mocks for
fetchWithDispatcher and graceful-git existed, but beforeEach restored the
real implementations. When GitHub throttles the shared CI runner IPs, the
HEAD probe in isRepoPublic() fails (it has zero retries and treats any
error as "private"), and resolution silently degrades from the hosted
tarball to a git clone, changing the resolved id and failing the
assertions. This broke the main branch build at
https://github.com/pnpm/pnpm/actions/runs/29026897310/job/86153736091
The mocks are now the default: fetch reports every repository as public
and graceful-git serves ls-remote output from a fixture table captured
from the real repositories, with the same commit hashes the assertions
already expected. The private-repo-over-HTTPS test now calls
mockFetchAsPrivate() explicitly instead of relying on a real 404 for the
nonexistent github.com/foo/bar. The one live-network case in
parsePref.test.ts got the same treatment. The suite drops from ~40s to
under half a second and runs offline.
The dlx e2e test stays a genuine end-to-end test against GitHub, but its
allowBuild list now approves both resolution shapes of the same commit
(codeload tarball and git+https clone), so the resolver's
rate-limit-induced fallback no longer trips the
GIT_DEP_PREPARE_NOT_ALLOWED gate, as seen in
https://github.com/pnpm/pnpm/actions/runs/29029971938/job/86170695840
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The recursive command runners sorted opts.selectedProjectsGraph, which keeps
only the selected projects as keys. Edges to unselected projects were dropped,
so a transitive dependency between two selected projects (a -> b -> c with only
a and c selected) was lost and the two ran out of order.
sortFilteredProjects resolves the order through the full workspace graph: for
each sorted project it walks dependencies, tunneling past unselected projects
and stopping at selected ones, so a transitive relationship becomes a direct
edge. Projects without a real dependency stay in the same chunk, preserving
concurrency.
Prod-only filters (--filter-prod) prune dev edges. Their selected projects are
sorted through the prod-pruned full graph, so transitive prod deps are honored
without reintroducing the dropped dev edges. In a mixed selection each project
is sorted through the graph that matches how it was selected: regularly filtered
projects through the full graph, prod-only ones through the prod-pruned graph.
Carrying prod-only projects on the full graph would let a dev-only reverse edge
form a cycle and collapse a real prod dependency and its dependent into one
chunk.
Fixespnpm/pnpm#8335
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
The dlx cache path is <cacheDir>/dlx/<key>/<prepare>/node_modules/.pnpm/
<pkgId>/node_modules/<pkg>. The <key> (64-char sha256 hex) and <prepare>
(<time>-<pid> in hex) segments are dlx overhead on top of pnpm's already-
deep virtual-store layout. For a transitive dep with a long name this tips
the package directory over Windows' MAX_PATH (260) — measured at 259 chars
for @pnpm.e2e/pre-and-postinstall-scripts-example in the dlx e2e test. A
lifecycle script then runs with that directory as its cwd, and CreateProcess
fails to resolve an over-length cwd, which Node surfaces as the confusing
"spawn C:\Windows\system32\cmd.exe ENOENT". Flaky rather than constant
because the <prepare> and temp-dir segments vary in length, straddling 260.
Shorten both dlx-specific segments:
- cache key: createShortHash (32 hex, 128 bits) instead of createHexHash
(64). pacquet already truncated to 32, so this also restores parity.
- prepare dir: encode time and pid in base36 instead of hex.
The TypeScript pnpm CLI freezes at v11; pnpm 12 will be the Rust pacquet
port. To make that split legible, all TypeScript source, test, and build
directories move under a new top-level pnpm11/ directory. The name states
the version boundary rather than implying a behavioral fork, since the two
stacks are meant to behave identically.
Scope is source-only: the shared workspace root stays at the repo root.
pnpm-workspace.yaml, package.json, pnpm-lock.yaml, .pnpmfile.cjs,
.meta-updater, __patches__, .changeset, .husky, and the lint/spell configs
remain in place, so one pnpm workspace and one Cargo workspace still span
all three products. pnpr/client and pacquet/tasks/registry-mock stay as
cross-product workspace members.
Rewiring the move required:
- pnpm-workspace.yaml globs prefixed with pnpm11/
- root package.json script paths, eslint.config.mjs, tsconfig.lint.json,
.gitignore, and CODEOWNERS updated
- .meta-updater/src/index.ts literals repointed (pnpm11/pnpm/package.json,
pnpm11/__utils__, pnpm11/__typings__, and the main package directory)
- regenerated every moved package's repository/homepage URL via meta-updater
- pnpm11/pnpm/bundle-deps.ts and __utils__/scripts/src/typecheck-only.ts
climb one more level to reach the repo root
.meta-updater stays at the repo root because @pnpm/meta-updater resolves
its config at <cwd>/.meta-updater/main.mjs.
TS CI (.github/workflows/ci.yml) now only runs when pnpm11/-relevant paths
change, via a dorny/paths-filter changes job plus a TS CI / Success
aggregate gate; branch protection should require only that gate.