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.
Remove the legacy repository changelog files now that release changelog storage defaults to the registry. The publish path composes and injects CHANGELOG.md into release tarballs, so keeping historical copies in source control duplicates generated release data.
Update adm-zip to the patched 0.6 release and override vulnerable transitive versions after the dependency audit began rejecting versions below 0.6.0.
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>
Pure directory move plus path fixups: the Rust port ships as pnpm v12,
so the source tree now lives at pnpm/ (alongside pnpm11/, the frozen
TypeScript line). No identifiers change in this pass — crate names
(pacquet-*), the pacquet bin, PACQUET_VERSION, the @pacquet/* npm
package names v11's runPacquet spawns, the .pacquet virtual-store dir,
the benchmark harness's clone dir, and the pacquet-*.yml workflow
filenames (npm trusted publishing is bound to them) all stay for a
follow-up.
Also removes the root /pnpm/ .gitignore entry (build detritus in the
pre-pnpm11 package location): pnpm/ is real source now and must not be
ignored. Developers with a stale generated pnpm/ dir should delete it
before checking out this change.
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.
* fix(dlx): make failed-install cache cleanup best-effort
On Windows, `pnpm dlx` could fail with a spurious "EBUSY: resource busy
or locked, rmdir" error. When an install into the dlx cache failed, the
catch block removed the partially-populated prepare dir with
`fs.promises.rm(cachedDir, { recursive: true, force: true })`. That call
has no retries, so it died on the same lingering Windows handle (a
just-run install script's child process, or antivirus scanning freshly
written files) and threw EBUSY — which then replaced and masked the
original install error.
Make the cleanup best-effort: swallow its failure so the original error
always surfaces, and add `maxRetries`/`retryDelay` so the removal itself
succeeds once the transient lock clears. A leftover prepare dir is
harmless — it has a unique name and findCache only trusts the `pkg`
symlink.
The pacquet port already removes the prepare dir best-effort (`let _ =
fs::remove_dir_all(...)`) and returns the original error, so the
user-visible behavior already matches; only the (non-observable) retry
is absent there.
* fix(dlx): log dlx cache cleanup failures instead of swallowing them
Catch the best-effort cache cleanup with a narrow handler that logs the
failure via logger.warn (mirroring tryRemovePkg in modules-cleaner's
prune) instead of a blanket `.catch(() => {})`. The original install
error is still the one rethrown, so cleanup failures stay visible without
ever masking the real cause.
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.