Resolves the 15 open alerts on https://github.com/pnpm/pnpm/security/code-scanning by addressing all four categories that CodeQL flagged.
### Prototype-polluting assignment (3 alerts, product code)
- `pkg-manifest/utils/src/convertEnginesRuntimeToDependencies.ts`: the inner write now dispatches over a literal `switch` on `runtimeName`, so the assignment is always keyed by `'node' | 'deno' | 'bun'`.
- `pkg-manifest/utils/src/updateProjectManifestObject.ts`: added an `isProtoPollutionKey` barrier at the top of the loop so `packageSpec.alias` can never reach the dynamic property write with `__proto__` / `constructor` / `prototype`.
- `installing/deps-installer/src/uninstall/removeDeps.ts`: the package list is filtered through `isProtoPollutionKey` once up front, and the dependency record is captured into a local before the loop.
### Polynomial ReDoS (2 alerts)
- `deps/inspection/list/src/renderDependentsTree.ts`: `replace(/\n+$/, '')` swapped for a constant-time `charCodeAt` trim.
- `resolving/npm-resolver/src/fetch.ts`: removed the super-linear-backtracking `semverRegex` and replaced it with an O(n) `stripTrailingSemverSuffix` that splits on the rightmost `@` and `semver.valid`s, with a digit-block fallback so `foo1.0.0`-style names still produce the existing "Did you mean foo?" hint.
### Bad code sanitization (8 alerts, test infrastructure)
- `__utils__/test-ipc-server/src/TestIpcServer.ts`: the `JSON.stringify(...).slice(1, -1)` smell at the source of all 8 test-file alerts is gone. Both `sendLineScript` and `generateSendStdinScript` now build the JS source with plain `JSON.stringify` and delegate shell wrapping to a new `wrapNodeEval` helper that escapes `\\` and `"` for the outer double-quoted shell argument.
### Incomplete sanitization (2 alerts, test file)
- `releasing/commands/test/publish/oidcProvenance.test.ts`: `.replace('/', '%2f')` → `.replaceAll(...)` on both flagged lines.
- `pnpm publish` failed to complete the web-based authentication flow when an HTTP/HTTPS proxy was configured. `libnpmpublish` (used for the initial publish request) routes through the proxy, but the subsequent `doneUrl` polling went through `@pnpm/network.fetch` without forwarding any proxy/TLS settings. The registry rejected the poll with `403` because the source IP differed from the initial request, so publish hung on the QR-code prompt forever.
- Adds `createDispatchedFetch(opts)` to `@pnpm/network.fetch` — a curried `fetchWithDispatcher` that pre-binds proxy / TLS / local-address / `configByUri`-derived client certificates. `publishPackedPkg` uses it to build an `OtpContext` whose `fetch` honors the same network configuration as the publish request.
- `extractTlsConfigs` is now performed automatically inside `createDispatchedFetch` (and hoisted out of the per-request loop in `createFetchFromRegistry`), so callers only have to pass `configByUri` once.
Fixes#11561.
- Fixes [#11519](https://github.com/pnpm/pnpm/issues/11519): `pnpm pack` in pnpm 11 silently dropped every package listed in `bundleDependencies` / `bundledDependencies`, producing tarballs that no longer contained the bundled `node_modules/<dep>` files that v10 produced.
- Root cause: the npm-packlist v10 upgrade ([#10658](https://github.com/pnpm/pnpm/pull/10658)) changed its API to require the caller to pre-populate the dependency tree's `edgesOut` Map. The wrapper in `fs/packlist` passed an empty Map, so npm-packlist's `gatherBundles()` looked up each declared name, found nothing, and skipped them all.
- Fix: `fs/packlist` now reads each bundled dep's `package.json` (walking up parent `node_modules` to support hoisted layouts), recursively populates `edgesOut` for transitive deps of bundled packages, and normalizes `bundleDependencies: true` to an explicit array (npm-packlist iterates the field directly).
When a published version contained a `+<build>` segment (e.g.
`1.0.0-canary.0+abc1234`), `pnpm publish --provenance` was rejected by
the registry with a 422 verifying the sigstore provenance bundle.
`libnpmpublish.publish()` runs `semver.clean()` on `manifest.version`,
which strips build metadata, before computing the provenance subject.
pnpm was packing the tarball with the original version, so the version
embedded in the packed `package.json` no longer matched the version in
the metadata payload and the bundle's subject — causing the registry to
reject the publish.
Strip build metadata from the published version after creating the
publish manifest, then derive both the tarball filename and the
manifest packed inside the tarball from that cleaned version.
Closes#11518.
## Summary
`pnpm publish` will now let an OIDC-derived token from npm's trusted publishing flow take precedence over a statically configured `_authToken` (e.g. one written from an `NPM_TOKEN` CI secret), mirroring the [npm CLI's behavior](https://github.com/npm/cli/blob/7d900c46/lib/utils/oidc.js).
### Background
We noticed that `pnpm@11.0.0-alpha.5` was published with trusted publishing on npm (its registry metadata has `_npmUser.trustedPublisher` and a SLSA `attestations.provenance` block) — but `pnpm@11.0.6` was not (`_npmUser: pnpmuser`, no attestations). The two were published by different clients: alpha.5 went out via `npm publish` (its metadata carries `_npmVersion: 11.6.2`, which `pnpm publish` never sets), while 11.0.6 went out via `pn release` from `.github/workflows/release.yml`.
Both paths run in CI with `id-token: write` granted, and both have the same `pn config set //registry.npmjs.org/:_authToken "\${NPM_TOKEN}"` step before publish. The difference is purely in how each client orders the auth precedence:
- **npm CLI**: tries the OIDC exchange first; on success **overwrites** the configured `_authToken`. The static token only acts as a fallback when OIDC isn't applicable (no trusted publisher configured, exchange fails, etc.).
- **pnpm publish (before this PR)**: bailed out of OIDC entirely as soon as a token was already configured (`releasing/commands/src/publish/publishPackedPkg.ts`, the old `fetchTokenAndProvenanceByOidcIfApplicable`). Worse, the call site used `??=` so OIDC could never overwrite the static token even if the bail-out had been removed.
So even though pnpm has a complete, working OIDC implementation, any release workflow that still wired in `NPM_TOKEN` silently downgraded to legacy token-based publishing — no `trustedPublisher` on the metadata, no provenance attestation. That's the fix here.
## Changes
- \`fetchTokenAndProvenanceByOidcIfApplicable\` → \`fetchTokenAndProvenanceByOidc\`. Dropped the now-unused \`targetPublishOptions\` parameter and removed the early bail-out — OIDC is always attempted when running in a supported CI environment with \`id-token: write\`.
- At the call site in \`createPublishOptions\`: when OIDC returns an authToken, it now overwrites \`publishOptions.token\` instead of nullish-assigning. The static token still wins when OIDC isn't applicable (no CI env, no trusted publisher configured, exchange fails).
- \`appendAuthOptionsForRegistry\` propagates the (possibly OIDC-overridden) token to the registry-scoped \`\${configKey}:_authToken\`, so libnpmpublish picks it up correctly.
- A \`ProvenanceError\` from \`determineProvenance\` no longer discards the freshly-fetched OIDC authToken — the publish itself can still go through with the OIDC token, again matching npm CLI behavior.
- Exported \`fetchTokenAndProvenanceByOidc\` (marked \`@internal\`) so the precedence rules are unit-testable.
### Recursive publish
The recursive publish loop in \`recursivePublish.ts\` calls \`publishPackedPkg\` once per workspace package, and OIDC token exchange is package-scoped on the npm side (\`/-/npm/v1/oidc/token/exchange/package/\${name}\`). With this fix, every workspace package independently attempts trusted publishing and only falls back to the static token if its own exchange fails. No structural change needed there.
pnpm 10 transitively emitted an npm-CLI-shaped per-package JSON object on stdout for
`pnpm publish --json` because the publish command shelled out to npm. pnpm 11 (#10591)
reimplemented publish natively and the new handler returned undefined, so `--json` now
produces 0 bytes on stdout on success — silently breaking downstream tooling that grepped
that contract. The most impactful regression is `nx release publish`, which parses
stdout JSON to confirm success; under `--nx-bail=true` it aborts the rest of a release
run when JSON is missing (see nrwl/nx#35575).
This restores the contract for both single-package and recursive publish, and aligns
`--report-summary` to use the same per-package shape:
- `pnpm publish --json` → single object `{ id, name, version, size,
unpackedSize, shasum, integrity, filename, files,
entryCount, bundled }`, mirroring `npm publish --json`.
- `pnpm publish -r --json` → array of those objects, mirroring `pnpm pack --json`'s
shape choice for single vs multi.
- `pnpm publish -r --report-summary` → existing `pnpm-publish-summary.json` envelope
`{ publishedPackages: [...] }` is preserved, but
each entry is upgraded to the same per-package shape
(additive — `name` and `version` are still present).
Implementation:
- `pack.api()` computes `unpackedSize` while it still has the filesMap.
- `publishPackedPkg` returns a `PublishSummary` (SHA-1 `shasum` + SHA-512 `integrity`
via `node:crypto`, no new deps) instead of `void`.
- `bundled` normalizes `bundledDependencies` / `bundleDependencies` per npm's semantics
(including `true` → all dep names).
- `recursivePublish` collects per-package `PublishSummary` objects and returns them; the
existing manifest-pick fallback is kept for paths that don't produce a summary.
- `publish.handler` returns `{ output, exitCode }` with the serialized summary (object
or array) when `opts.json` is set; main.ts already writes `output` to stdout.
Refs #11476
Refs nrwl/nx#35575
Amp-Thread-ID: https://ampcode.com/threads/T-019df90f-8f75-763f-b528-4602e870a972
Co-authored-by: Charlie Croom <charlie+amp@noreply.com>
Co-authored-by: Amp <amp@ampcode.com>
The native publish flow introduced in v11 (replacing the `npm publish`
shell-out with `libnpmpublish`) only read the registry from `registries`
config, ignoring `publishConfig.registry` from the package's
`package.json`. Restore the prior behavior by giving
`publishConfig.registry` precedence over the configured registries.
Closes#11419
* chore: upgrade @typescript/native-preview to 7.0.0-dev.20260421.2
- Add explicit `types: ["node"]` to the shared tsconfig because tsgo
20260421 no longer auto-acquires `@types/*` from `node_modules`.
- Refactor test files to explicitly import jest globals (`describe`,
`it`, `test`, `expect`, `beforeEach`, etc.) from `@jest/globals`
instead of relying on `@types/jest` ambient declarations. Under the
new tsgo build, `import { jest } from '@jest/globals'` shadows the
ambient `jest` namespace, breaking `@types/jest`'s `declare var
describe: jest.Describe;` globals.
- Add `@jest/globals` to each package's devDependencies where tests
now import from it, and add `@types/node` to packages that need it
but were relying on hoisted resolution.
- Replace `fail()` calls with `throw new Error(...)` since `fail` is
no longer globally available.
* chore: fix remaining tsgo type-strictness errors
- Strip `as <PnpmType>` casts on objects passed to toMatchObject /
toStrictEqual / toEqual; @jest/globals rejects the typed objects
(which include AsymmetricMatchers) vs. the repo-specific type.
- Type `jest.fn<...>()` explicitly where the mock's signature matters
for toHaveBeenCalledWith.
- Replace `beforeEach(() => X)` with `beforeEach(() => { X })` so the
return value is void, as the stricter jest typing requires.
- Use `expect.objectContaining({...})` in one place where the full
expected object triggered stricter type resolution.
- Cast `prompt.mock.calls` arg through `as unknown as Record<...>[]`
for patch.test.ts's nested-array matchers.
- Fix off-by-one `<reference path>` in pnpm/test/getConfig.test.ts
that only surfaced now.
- Move `@jest/globals` from devDependencies to dependencies in the
two `__utils__` packages that import it from `src/`.
- Clean up unused imports from the @jest/globals migration.
* chore: address Copilot review on #11332
- Move misplaced `@jest/globals` imports to the top import block in
checkEngine, run.ts, and workspace/root-finder tests where the
script dropped them below executable code.
- Replace `try { await x(); throw new Error('should have thrown') } catch`
in bins/linker, lockfile/fs, and resolving/local-resolver tests with
`await expect(x()).rejects.toMatchObject({...})`. The old pattern
swallowed an unrelated `throw` if the under-test call silently
succeeded, which would fail on the catch-block assertion with a
misleading message.
## Summary
`@pnpm/exe@11.0.0-rc.4` aborts on every invocation with:
```
node::sea::(anonymous namespace)::SeaDeserializer::Read() at ../src/node_sea.cc:174
Assertion failed: (format_value) <= (static_cast<uint8_t>(ModuleFormat::kModule))
```
Two independent Node.js v25.7+ SEA regressions are responsible, both surfaced by the rc.4 bump of the embedded runtime from 25.6.1 to 25.9.0. This PR fixes both and adds a prepublish smoke test so a broken binary can't reach npm again.
## Root cause
**1. SEA blob format changed in Node.js v25.7.0** ([nodejs/node#61813](https://github.com/nodejs/node/pull/61813) added ESM-entry-point support and inserted a new `ModuleFormat` byte into the blob header). SEA blobs carry no version marker, so a blob written by one Node.js version can only be deserialized by a matching one. In rc.4, the CI host Node.js (25.6.1, pre-change) wrote the blob and it was embedded in a 25.9.0 runtime (post-change) — the deserializer reads a misaligned byte as `format_value`, exceeds `kModule`, `CHECK_LE` fires, `SIGABRT`. `resolveBuilderBinary()` was preferring `process.execPath` whenever the running Node supported `--build-sea`, never checking that its version matched the embedded runtime.
**2. Node.js v25.7+ replaces the ambient `require` and `import()` inside a CJS SEA entry with embedder hooks** that only resolve built-in module names. The `pnpm.cjs` shim loaded `dist/pnpm.mjs` via `await import(pathToFileURL(...).href)`, which after the fix to (1) reached the CJS entry and then blew up with:
```
ERR_UNKNOWN_BUILTIN_MODULE: No such built-in module: file:///.../dist/pnpm.mjs
at loadBuiltinModuleForEmbedder
at importModuleDynamicallyForEmbedder
```
## Changes
- **`releasing/commands/src/pack-app/packApp.ts`** — `resolveBuilderBinary` now takes the resolved target runtime version and only reuses `process.execPath` when `process.version` exactly matches; otherwise it downloads a host-arch Node of the target version via the existing `ensureNodeRuntime` path. Added `PACK_APP_RUNTIME_TOO_OLD` for runtimes older than v25.5 (no `--build-sea`). Removed the now-unused `DEFAULT_BUILDER_SPEC` and the stale `fetch`/`nodeDownloadMirrors` args on the builder resolver. Help text / examples refreshed to drop `node@22` / `node@lts` references that would now be rejected.
- **`pnpm/pnpm.cjs`** — loads `dist/pnpm.mjs` through `Module.createRequire(process.execPath)` instead of `await import(fileURL)`. `createRequire` returns a regular CJS loader that bypasses the SEA embedder hooks, and the pnpm bundle has no top-level await so synchronous `require` of ESM (Node 22+) loads it cleanly. No build-time paths are baked in — `process.execPath` is evaluated at runtime, verified by relocation-testing the darwin-arm64 SEA under `/tmp/`.
- **`pnpm/artifacts/verify-binary.mjs`** (new) + `prepublishOnly` on every platform artifact — replaces the existence-only `test -f pnpm` gate with:
1. A **relocation-sensitivity check**: run the binary without `dist/` staged and confirm the failure mentions a path derived from `process.execPath`, not a build-time constant. Catches any future regression of (2).
2. A **smoke test**: stage a `dist → ../exe/dist` symlink (using `symlink-dir` so Windows junctions are handled transparently), exec `./pnpm -v`, assert the output is a SemVer 2 string.
- Cross-platform targets (darwin/win32 artifacts on a Linux CI, or a libc mismatch) skip the exec with a log line and fall back to existence-only, so a musl artifact published from a glibc host still goes through.
- Real `dist/` dirs (developer layout) are preserved; stale symlinks from aborted runs are replaced; created symlinks are cleaned up on exit.
- **`pnpm/artifacts/exe/test/setup.test.ts`** — new `pnpm -v` execution test gated on both the platform binary and the staged bundle being present, so ordinary `pn compile` test runs skip cleanly instead of failing on a missing `dist/`.
## Summary
- **@pnpm/exe build tooling**: Replace the 253-line hand-rolled SEA builder in `pnpm/artifacts/exe/scripts/build-artifacts.ts` with a thin ~60-line driver that delegates to the new `pnpm pack-app` command (shipped in #11312). Per-platform defaults (entry, outputDir, outputName, targets) move to `pnpm.app` in `pnpm/artifacts/exe/package.json`.
- **pack-app CLI (breaking, unreleased)**: Rename `--node-version <v>` → `--runtime <name>@<v>`, and `pnpm.app.nodeVersion` → `pnpm.app.runtime`. Only `node@<v>` is accepted today; the `<name>@` prefix reserves room for `bun` / `deno` without a future breaking change.
- **Embedded Node**: Pin `node@25.9.0` in the build-artifacts driver.
## Why the rename
pack-app's old `--node-version` flag silently inherited from pnpm's global `node-version` rc setting (which controls which Node pnpm uses to run user scripts). That value leaked into `Config['nodeVersion']` and overrode `pnpm.app.nodeVersion`, causing the wrong Node build to be embedded in SEAs for users who had the rc key set — reproduced locally, where `linux-arm64-musl` failed entirely because Node 22.13.0 has no arm64-musl variant. `runtime` doesn't collide with any Config field, so CLI / `pnpm.app` / running-Node precedence now behaves as intended.
## Behavior parity with the old script
- Same output paths: `pnpm/artifacts/<target>/pnpm` (or `pnpm.exe` for `win32-*`).
- Same signing (pack-app uses `codesign` natively on macOS, `ldid` when cross-signing from Linux).
- Same host-based target narrowing: Linux/M1 Mac build the full 8-target matrix; Intel Mac builds only `win32-x64`, `linux-x64`, `darwin-x64`.
- Same `dist/` copy + `@reflink/*` prune into `pnpm/artifacts/exe/dist/` for npm publishing.
- Same bundle-first step (`pn --filter=pnpm prepublishOnly &&` prefix is unchanged in the `build-artifacts` npm script).
Library packages had `prepublishOnly: pn compile`, which expands to
`tsgo --build && pn lint --fix`. During `pn release` that runs eslint
against ~150 packages for no benefit — the code has already been linted
in CI and the release flow's upfront compile has already built dist/.
Switch lib prepublishOnly to a bare `tsgo --build` so the safety-net
compile stays but the per-package eslint cost is gone.
* fix: give each runtime variant its own global virtual store entry
When a runtime package (e.g. node@runtime:X.Y.Z) uses a variations
resolution, createFullPkgId() in @pnpm/deps.graph-hasher was hashing
the whole VariationsResolution — the same hash on every host — so the
global virtual store path collided between variants. Whichever variant
installed first won, and a later `pnpm add --libc=musl node@runtime:<v>`
silently reused the cached glibc (or macOS/Windows) binary.
The fix threads supportedArchitectures down to createFullPkgId so the
selected variant's integrity is used as the package fingerprint. Two
related cleanups land with it:
- Extract the platform-variant selection logic to @pnpm/resolving.resolver-base
as selectPlatformVariant/resolvePlatformSelector. The helper's libc
match also required a fix: a variant with no libc is the "default"
build, and a request for a non-default libc (e.g. musl) must require
an exact match so the default variant doesn't silently win.
- @pnpm/installing.package-requester's findResolution now delegates to
the shared helper, and the new supportedArchitectures param is plumbed
through calcDepState / calcGraphNodeHash / iterateHashedGraphNodes /
lockfileToDepGraph and their callers in deps-resolver, deps-restorer,
deps-installer, graph-builder, and building.after-install.
* feat: add pnpm build-sea command for building Node.js SEA executables
Adds `pnpm build-sea` under @pnpm/releasing.commands. Takes a CommonJS
entry file and a set of target triplets (linux-x64, linux-x64-musl,
linux-arm64, linux-arm64-musl, macos-x64, macos-arm64, win-x64,
win-arm64) and produces a standalone executable per target under
dist-sea/<target>/.
Each target's Node.js runtime is fetched via `pnpm add node@runtime:<v>
--os=<os> --cpu=<arch> --libc=<libc>` into $PNPM_HOME/build-sea/<target>-<v>/
so binaries are hardlinked from the global content-addressable store and
`pnpm store prune` can reclaim them.
Requires Node.js v25.5+ to perform the --build-sea injection. If the
running Node is older, a v25 binary is downloaded and used as the builder
automatically. macOS outputs are ad-hoc signed with codesign (on macOS)
or ldid (when cross-compiling from Linux), which is required because SEA
injection invalidates the binary's existing signature.
* fix(build-sea): reject malformed --target, --output-name and use mkdtemp for config
Addresses Copilot review feedback on the build-sea command:
- parseTarget() previously destructured the target string, silently
accepting extra `-` segments. Inputs like `linux-x64-musl-../../outside`
would pass validation and flow into path.join. Validation is now done
with a strict anchored regex.
- --output-name was passed into path.join() without sanitization, so a
caller could escape the output directory with path separators or `..`.
validateOutputName() now rejects anything that isn't a plain basename.
- The per-target SEA config file was written to a predictable path under
os.tmpdir() (derived from the target name and Date.now()), which is
unsafe on multi-user systems. It now lives inside a fresh mkdtemp()
directory and is opened with the exclusive "wx" flag.
- New test cases cover extra-segment targets, uppercase/whitespace
variants, and the full matrix of invalid --output-name inputs.
* rename: build-sea → pack-app
`build-sea` required knowing what a SEA is. `pack-app` is self-describing,
doesn't collide with pnpm's existing `bin` concept, and parallels the
existing `pack` command.
- Command name: build-sea → pack-app
- Default output dir: dist-sea → dist-app
- Error codes: PACK_APP_* (was BUILD_SEA_*)
- Export/type: packApp / PackAppOptions (was buildSea / BuildSeaOptions)
- Install cache dir: $PNPM_HOME/pack-app (was $PNPM_HOME/build-sea)
The Node.js `--build-sea` flag name itself is unchanged — that's a
Node.js feature and outside this project's naming.
* fix(pack-app): reject directory entries, pin builder to >=25.5, refuse macOS target on Windows
Addresses Copilot review feedback on the pack-app command:
- entry validation now rejects non-file paths (directories, symlinks to
non-files) with a dedicated PACK_APP_ENTRY_NOT_FILE instead of
surfacing a less actionable error later in the SEA build.
- DEFAULT_BUILDER_SPEC was the bare major ("25"), which would satisfy
with 25.0.x if that version is still present — those point releases
predate --build-sea support. Tightened to ">=25.5.0 <26.0.0" so the
download is guaranteed to support the flag without ever crossing a
major.
- adHocSignMacBinary() silently skipped re-signing on Windows hosts.
Now throws PACK_APP_MACOS_SIGN_UNSUPPORTED_HOST with a hint to build
the target on macOS/Linux or re-sign manually.
- resolvePlatformSelector() JSDoc now matches what the code actually
does (picks the first entry when it is not "current"; later entries
are ignored).
- New test case covers the directory-as-entry rejection.
* refactor(pack-app): switch target OS names to process.platform constants
Previously `pack-app` accepted `macos-*` / `win-*` as the OS portion of a
target triplet and translated them to `darwin` / `win32` internally. The
translation layer made the CLI surface inconsistent with the values that
`pnpm add --os=…` and `supportedArchitectures.os` already use, and added
a small footgun (e.g. users setting `supportedArchitectures: { os: [darwin] }`
but typing `macos-arm64` for pack-app).
The supported target OS set is now `linux | darwin | win32`, matching
`process.platform`. Old inputs like `macos-arm64` or `win-x64` now fail
validation with a clear error pointing to the new naming. The internal
parseTarget helper drops its TARGET_OS_MAP lookup entirely.
This is a change to an unreleased command so there is no back-compat
concern. pnpm's own artifact directory names (`pnpm/artifacts/macos-*/`,
`pnpm/artifacts/win-*/`) are an internal implementation detail and are
not affected by this change.
* feat(pack-app): read defaults from pnpm.app in package.json
Every pack-app flag (--entry, --target, --node-version, --output-dir,
--output-name) can now be preconfigured in the project's package.json
under a new "pnpm.app" object:
{
"name": "my-cli",
"pnpm": {
"app": {
"entry": "dist/index.cjs",
"targets": ["linux-x64", "darwin-arm64", "win32-x64"],
"nodeVersion": "25",
"outputDir": "release",
"outputName": "my-cli"
}
}
}
CLI flags always win. --target replaces the configured list rather than
appending, so a user can narrow the default set at the command line.
The config loader is strict: unknown keys under pnpm.app and any
type-mismatched values throw PACK_APP_INVALID_CONFIG so mistakes surface
at invocation time instead of silently being ignored.
Chose pnpm.app over pnpm.packApp because it's the shorter, cleaner
namespace for anything related to the app bundle (future sibling
commands like run-app / deploy-app could share the same object without
a naming clash). Chose package.json over pnpm-workspace.yaml because
the config is inherently per-project, whereas pnpm-workspace.yaml is
workspace-root-only.
* fix(pack-app): deterministic libc selection and stricter output-name validation
Addresses Copilot review feedback:
- ensureNodeRuntime() now always passes an explicit --libc for linux
targets. Without a suffix, linux-x64 and linux-arm64 default to
--libc=glibc instead of letting the user's supportedArchitectures.libc
config or the host's detected libc decide the variant. The install
cache directory mirrors this, so glibc and musl variants are always
distinct (linux-x64-glibc vs linux-x64-musl).
- resolveBuilderBinary() now pins the host libc when downloading a
builder Node on Linux. A user whose config sets supportedArchitectures.libc
to musl no longer ends up with a musl Node that the glibc host cannot
execute.
- validateOutputName() rejects Windows-invalid filename characters
(<>:"|?* and NUL), Windows reserved device names (CON, NUL, COM1, etc.),
and names ending in a dot or space — problems surface at invocation
time rather than during writeFile(outputFile, ...) on Windows.
- lockfileToDepGraph variants tests no longer derive the "host"
variant from process.platform/process.arch; they always pass an
explicit supportedArchitectures selector so the expectations hold on
any CI host (including Alpine/musl).
* chore: add "toctou" to cspell wordlist
`TOCTOU` (time-of-check-to-time-of-use) is the standard term for the
race-condition class the pack-app SEA-config comment describes. Adding
it to the wordlist unblocks the Lint CI step.
* fix: lint
* fix: support explicit versions and --no-commit-hooks / --no-git-tag-version in `pnpm version`
Registers options under their canonical names so nopt correctly parses the
`--no-*` variants, accepts an explicit semver argument alongside bump types,
and creates a git commit + annotated tag for the bump (honoring
`--no-git-tag-version`, `--no-commit-hooks`, `--sign-git-tag`, `--message`,
and `--tag-version-prefix`). Also fixes `--no-git-checks` which was parsed
incorrectly. Closes#11271.
* chore: add gpgsign and newversion to cspell; set gpg sign config in version test
* fix: address Copilot review feedback on pnpm version
* fix: skip git commit and tag in recursive version runs
* fix: address Copilot review feedback on pnpm version
* feat: add pnpm docs command and home alias
* chore: update manifests
* fix: address review comments for pnpm docs command
- Remove 'docs' and 'home' from NOT_IMPLEMENTED_COMMANDS to prevent
not-implemented handlers from overriding the real implementations
- Change fallback URL from npmjs.com to npmx.dev
- Remove bugs URL as a docs fallback (it's an issue tracker, not docs)
- Fix ESM mock in tests: use jest.unstable_mockModule instead of jest.mock
* refactor: use open package for browser opening in promptBrowserOpen
Replace the hand-rolled platform-specific execFile logic with the open
npm package, which handles WSL, Docker-in-WSL, and Windows edge cases
better. This removes the execFile dependency injection from
promptBrowserOpen, OtpContext, LoginContext, and SharedContext.
* fix: address copilot review comments
- docs: use www.npmjs.com fallback (not npmx.dev) and validate homepage
URL is http(s) before opening
- promptBrowserOpen: validate authUrl protocol before passing to open(),
and guard against synchronous throws from open()
* fix: restore npmx.dev fallback for pnpm docs
* chore: expand changeset with web-auth refactor impact
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
* test: update registry-mock to 6.0.0 stable and use pnpm view in tests
Update @pnpm/registry-mock from 6.0.0-6 to 6.0.0 stable release.
Replace npm view with pnpm view in test helpers now that pnpm has
native view/dist-tag commands. Unskip the nodeRuntime test that was
blocked on the registry-mock republish.
* chore: update pnpm to beta 8
* feat: support versions, dist-tags, and time field selectors in pnpm view
The view command now exposes versions (as an array of version strings),
dist-tags, and time from registry metadata. Single-field --json output
returns the raw value instead of wrapping it in an object, matching npm
behavior. This allows tests to use pnpm view instead of npm view.
- Update `@pnpm/registry-mock` from 5.2.4 to 6.0.0-6
- Fix auth tests to use bearer token from `globalSetup` instead of hardcoding credentials
- Replace hardcoded integrity checksums with `getIntegrity()` from registry-mock in `customResolvers` tests
- Add `prepareFixtureWithIntegrity()` helper in deps-restorer tests to dynamically patch `@pnpm.e2e` integrity values in fixture lockfiles at runtime, so they don't go stale when registry-mock is updated
- Fix `workspace-external-depends-deep` fixture's current lockfile (was missing `packages/f` and `packages/g` importers)
- Remove unnecessary credentials from `gitChecks` tests (they reject before any registry interaction)
Implement dist-tag ls, add, and rm subcommands natively instead of
delegating to npm. Follows the same pattern as the recently added
deprecate and unpublish commands.
12 command test suites had near-identical ~50-field DEFAULT_OPTS objects
copy-pasted between them. Extract the common fields into a single shared
package so each suite only declares its overrides.
* refactor(config): split Config interface into settings + runtime context
Create ConfigContext for runtime state (hooks, finders, workspace graph,
CLI metadata) and keep Config for user-facing settings only. Functions
use Pick<Config, ...> & Pick<ConfigContext, ...> to express which fields
they need from each interface.
getConfig() now returns { config, context, warnings }. The CLI wrapper
returns { config, context } and spreads both when calling command
handlers (to be refactored to separate params in follow-up PRs).
Closes#11195
* fix: address review feedback
- Initialize cliOptions on pnpmConfig so context.cliOptions is never undefined
- Move rootProjectManifestDir assignment before ignoreLocalSettings guard
- Add allProjectsGraph to INTERNAL_CONFIG_KEYS
* refactor: remove INTERNAL_CONFIG_KEYS from configToRecord
configToRecord now accepts Config and ConfigContext separately, so
context fields are never in scope. Only auth-related Config fields
(authConfig, authInfos, sslConfigs) need filtering.
* refactor: eliminate INTERNAL_CONFIG_KEYS from configToRecord
configToRecord now receives the clean Config object and explicitlySetKeys
separately (via opts.config and opts.context), so context fields are
never in scope. main.ts passes the original split objects alongside
the spread for command handlers that need them.
* fix: spelling
* fix: import sorting
* fix: --config.xxx nconf overrides conflicting with --config CLI flag
When `pnpm add` registers `config: Boolean`, nopt captures
--config.xxx=yyy as the --config flag value instead of treating it
as a nconf-style config override. Fix by extracting --config.xxx args
before nopt parsing and re-parsing them separately.
Also rename the split config/context properties on the command opts
object to _config/_context to avoid clashing with the --config CLI option.
Major cleanup of the config system after migrating settings from `.npmrc` to `pnpm-workspace.yaml`.
### Config reader simplification
- Remove `checkUnknownSetting` (dead code, always `false`)
- Trim `npmConfigTypes` from ~127 to ~67 keys (remove unused npm config keys)
- Replace `rcOptions` iteration over all type keys with direct construction from defaults + auth overlay
- Remove `rcOptionsTypes` parameter from `getConfig()` and its assembly chain
### Rename `rawConfig` to `authConfig`
- `rawConfig` was a confusing mix of auth data and general settings
- Non-auth settings are already on the typed `Config` object — stop duplicating them in `rawConfig`
- Rename `rawConfig` → `authConfig` across the codebase to clarify it only contains auth/registry data from `.npmrc`
### Remove `rawConfig` from non-auth consumers
- **Lifecycle hooks**: replace `rawConfig: object` with `userAgent?: string` — only user-agent was read
- **Fetchers**: remove unused `rawConfig` from git fetcher, binary fetcher, tarball fetcher, prepare-package
- **Update command**: use `opts.production/dev/optional` instead of `rawConfig.*`
- **`pnpm init`**: accept typed init properties instead of parsing `rawConfig`
### Add `nodeDownloadMirrors` setting
- New `nodeDownloadMirrors?: Record<string, string>` on `PnpmSettings` and `Config`
- Replaces the `node-mirror:<channel>` pattern that was stored in `rawConfig`
- Configured in `pnpm-workspace.yaml`:
```yaml
nodeDownloadMirrors:
release: https://my-mirror.example.com/download/release/
```
- Remove unused `rawConfig` from deno-resolver and bun-resolver
### Refactor `pnpm config get/list`
- New `configToRecord()` builds display data from typed Config properties on the fly
- Excludes sensitive internals (`authInfos`, `sslConfigs`, etc.)
- Non-types keys (e.g., `package-extensions`) resolve through `configToRecord` instead of direct property access
- Delete `processConfig.ts` (replaced by `configToRecord.ts`)
### Pre-push hook improvement
- Add `compile-only` (`tsgo --build`) to pre-push hook to catch type errors before push
Replace the unmaintained @pnpm/npm-conf package with a purpose-built
module that reads only auth/registry-related settings from .npmrc files
using read-ini-file + @pnpm/config.env-replace (both already deps).
All non-registry settings (hoist-pattern, node-linker, etc.) are now
only read from pnpm-workspace.yaml, CLI options, or environment
variables. Registry-related settings (auth tokens, registry URLs,
SSL certs, proxy settings) continue to be read from .npmrc for
migration compatibility, and can also be set in pnpm-workspace.yaml.
New modules:
- loadNpmrcFiles.ts: reads .npmrc from standard locations, filters to
auth/registry keys, returns structured layers
- npmConfigTypes.ts: inlined npm config type definitions
- npmDefaults.ts: inlined npm defaults (registry, unsafe-perm, etc.)
* feat(auth): add "Press ENTER to open in browser" during web authentication
During web-based authentication (login, publish), users can now press
ENTER to open the authentication URL in their default browser. The
background token polling continues uninterrupted, so users who prefer
to authenticate on their phone can still do so without pressing anything.
The implementation uses Node's readline module (not raw mode), so Ctrl+C
and Ctrl+Z continue to work normally. It is fully error-tolerant: if the
keyboard listener or browser opening fails, a warning is printed and the
polling continues.
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* fix(auth): inject readline and execFile directly, not wrapper functions
Address review feedback:
- Remove defaultListenForEnter and defaultOpenBrowser wrapper functions
- Inject readline module and execFile function directly via context
- DEFAULT_CONTEXT now references modules directly (no closures)
- Use switch for platform detection, default = no browser prompt
- Rename pollWithBrowserOpen → offerToOpenBrowser (clearer name)
- Add platform-specific tests (darwin, win32, linux, freebsd)
- Use PassThrough streams for stdin mocks in tests
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* fix(auth): fix CI type errors in test mocks
- Type jest.fn() mocks for readline.createInterface properly
- Use PassThrough streams for stdin mocks in releasing/commands tests
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* refactor(auth): use generic Stdin parameter to eliminate PassThrough in tests
Per review feedback, add a generic Stdin type parameter to context
interfaces. This ties process.stdin and readline.createInterface together
through the same type, so tests can use simple { isTTY: true } mocks
instead of requiring PassThrough streams.
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* fix(auth): propagate Stdin generic to releasing/commands OtpContext
The OtpContext in releasing/commands extends BaseOtpContext from
web-auth. Now that BaseOtpContext is generic, the local OtpContext
and publishWithOtpHandling must also be generic so tests can use
simple stdin mocks without PassThrough.
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* fix: sort imports in releasing/commands otp.ts
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* refactor(auth): use .bind() for readline injection instead of generics
Per review feedback, revert the generic Stdin approach and instead use
readline.createInterface.bind(null, { input: process.stdin }) as the
injectable dependency. This avoids generics proliferation while keeping
the context clean — no arrow functions or closures in DEFAULT_CONTEXT.
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* feat(publish): add "Press ENTER to open in browser" during publish OTP
Wire up createReadlineInterface and execFile in the publish
SHARED_CONTEXT so that pnpm publish also offers to open the browser
during web-based OTP authentication.
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* fix(auth): improve browser-open prompt message
Change "Press ENTER to open in browser..." to
"Press ENTER to open the URL in your browser."
The old message implied the user should press Enter. The new wording
presents it as an available action, not an instruction — users can
also scan the QR code or copy-paste the URL.
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* style: remove unnecessary arrow wrapper around createMockReadlineInterface
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* docs: explain why Enter keypress is fire-and-forget, not awaited
Add a comment explaining that only pollPromise is awaited — the Enter
listener is intentionally not part of a Promise.all. This prevents a
future refactor from reintroducing the npm bug where authentication
blocks until Enter is pressed, even when the user authenticates on
another device.
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* docs: add permalink to npm's Promise.all bug in comment
Link to the specific npm-profile commit (d1a48be4259) so the comment
remains accurate even if npm fixes the bug in the future.
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* fix: correct line numbers in npm-profile permalink (L85-L98)
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* style: apply review suggestion for npm-profile permalink format
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* style: remove duplicate line in npm-profile comment
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* fix: shadow global process instead of renaming to proc
Destructure as `process` (not `proc`) so the global `process` is
shadowed, preventing accidental direct access to it.
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* fix: merge process fields in test mock contexts
Restructure createMockContext to merge process fields instead of
replacing the entire object. Tests that only need to override
platform or stdin no longer need to redundantly provide the other.
Also adds a test for undefined platform (default: case).
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* fix: use Omit+Partial for process overrides in test mock contexts
The process field spread `...overrides?.process` merges at runtime but
TypeScript still requires all fields in the override type. Fix by typing
the process override as Partial via Omit<..., 'process'> & { process?: Partial<...> }.
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* refactor: extract a type alias
* refactor: extract MockContextOverrides type alias in remaining tests
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* refactor(auth): extract process types, use NodeJS.Platform, clean up tests
- Extract OfferToOpenBrowserProcess interface from inline process type
- Extract LoginProcess interface from inline process type in LoginContext
- Use NodeJS.Platform instead of string for platform fields (prevents typos)
- Rename simulateEnter → simulateEnterKeypress (clarify it's the key)
- Convert single-return functions to arrow expressions in tests
- Update test descriptions to say "Enter key" / "Enter keypress"
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* refactor(auth): rename offerToOpenBrowser → promptBrowserOpen
Per review feedback, "offer to open browser" was mouthful. Renamed
function, file, and all associated types (OfferToOpenBrowser* →
PromptBrowserOpen*).
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* docs: drop "IMPORTANT"
* refactor(auth): extract OtpProcess interface from inline process type
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* fix(auth): validate authUrl before passing to execFile
On Windows, cmd.exe re-parses execFile arguments with full shell
grammar, so metacharacters (&, |, ^, etc.) in the URL would be
interpreted as operators. Validate that authUrl is a well-formed
http(s) URL before passing it to the platform browser command.
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* test(auth): add regression test for URLs with query parameters on win32
Verifies that URLs containing & and other query string characters are
passed through to execFile as-is on the win32 platform.
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* fix(auth): escape cmd.exe metacharacters in Windows browser open URL
On Windows, cmd.exe re-parses execFile arguments and treats & | < > ^ %
as operators. Escape these with ^ so query strings in auth URLs
(e.g. ?token=abc&redirect=...) are not split by cmd.exe.
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* fix(auth): use canonicalized URL and expand cmd.exe escape set
- Use parsedUrl.href (canonicalized by new URL()) instead of the raw
authUrl string, ensuring percent-encoding of spaces and special chars.
- Expand cmd.exe metacharacter escaping to include () and ! in addition
to & | < > ^ %, covering grouping operators and delayed expansion.
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* docs(auth): document Windows browser-opening edge cases
Explain why cmd /c start is used instead of ShellExecuteW (not
callable from Node.js without a native addon), why alternatives
like explorer.exe, rundll32, and PowerShell are unreliable, and
note that a Rust/N-API addon could replace this in the future.
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
* fix: fix cspell errors in Windows browser-open comment
Reword to avoid unknown words "rundll" and "metacharacter".
https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: make clean/setup/deploy prefer user scripts over built-in commands
When a project's package.json has a script named "clean", "setup", or
"deploy", running `pnpm clean/setup/deploy` now executes the script
instead of the built-in command. This prevents surprising behavior for
users with existing scripts.
When running from a workspace subdirectory where the root package.json
has one of these scripts, an error is thrown with guidance on how to
proceed.
Added "purge" as an alias for the built-in clean command, which always
runs the built-in regardless of scripts.
Closes#6816
* feat: also make rebuild prefer user scripts over the built-in
* refactor: move scriptOverride to command definitions
Each command now declares `scriptOverride = true` instead of a
centralized list in main.ts. All command names including aliases
are overridable by same-named scripts.
* refactor: rename scriptOverride to overridableByScript
* test: add e2e tests for script override behavior in clean/purge
* fix: address review feedback
- Fix JSDoc to reflect that aliases are also overridable by scripts
- Update npm_command env var to 'run-script' when redirecting to run
- Add 'purge' alias to clean command help text
* fix: remove --workspace flag from version command, use only --recursive
The --workspace/--workspaces flags were incorrectly added as synonyms for
--recursive in the version command. In pnpm, --recursive (-r) is the
standard convention for applying commands across workspace packages.
Recursive versioning now only activates with the explicit -r flag.
* test: improve version command test coverage
Add tests for major/patch bumps, --json output, --allow-same-version,
invalid version handling, missing name/version, empty params, and
recursive mode with workspace packages including JSON output, skipping
unnamed packages, and verifying --recursive is required.
* test: use expect().rejects instead of try/catch in version tests
Avoids silent passes when the handler doesn't throw.