Skip to content

chore: migrate to pnpm 11 with supply-chain defaults #27

Description

@llbbl

Context

This package pins pnpm@10.34.5. Migrating to pnpm 11 adopts its supply-chain hardening defaults and consolidates onto the single global pnpm store.

pnpm 11 turns on five defaults that were previously opt-in or absent:

Setting Default Purpose
minimumReleaseAge 1440 (24h) Refuse versions younger than the cooldown window — blocks the publish-malware-then-wait-for-CI attack
blockExoticSubdeps true Transitive deps cannot come from git URLs or arbitrary tarballs
strictDepBuilds true Unapproved postinstall scripts fail the install
verifyDepsBeforeRun install Re-check lockfile before pnpm run
optimisticRepeatInstall true Skip work when the lockfile is unchanged

The Node 20 decision

pnpm 11 is pure ESM and requires Node >=22. This repo's CI matrix currently tests 20.x, 22.x, 24.x, so the Node 20 leg would fail at pnpm install as soon as the packageManager field moves.

Decision: drop Node 20 and raise engines.node to >=22.

Rationale:

  • Node 20 reached end-of-life in April 2026 and no longer receives security updates.
  • Leaving engines.node: ">=20" while CI no longer tests Node 20 would advertise support that isn't verified.

This is a support-contract change on a published package. Consumers still on Node 20 (polydoc, TeamWiki) will see an engines warning on install. The package is pre-1.0 (0.1.1), so this is a reasonable point to make the change.

release.yml already uses Node 22.x and needs no change.

Scope notes

  • Lockfile is already lockfileVersion: '9.0', identical between pnpm 10 and 11 — no regeneration expected.
  • No .npmrc, no package.json#pnpm field, no Dockerfile or devcontainer — nothing to relocate into the new config home.
  • Both workflows use pnpm/action-setup@v6 with no version: pin, so they read packageManager from package.json automatically.
  • strictDepBuilds means the first install is expected to fail with ERR_PNPM_IGNORED_BUILDS until each dependency with a postinstall script is audited and allowlisted.

Tasks

Verification note

check:package (attw + publint) is the critical gate here — packaging breakage is invisible to lint, typecheck, and the test suite, and only surfaces for a consumer after publish.

Follow-up (not in this change)

@types/node is pinned at ^20.19.43, which is inconsistent with an engines.node: ">=22" contract. Bumping it can surface new type errors, so it is left for a separate PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions