Skip to content

[osprey-ui] migrate from npm to pnpm via Corepack#252

Open
haileyok wants to merge 13 commits into
mainfrom
hailey/pnpm-migration
Open

[osprey-ui] migrate from npm to pnpm via Corepack#252
haileyok wants to merge 13 commits into
mainfrom
hailey/pnpm-migration

Conversation

@haileyok
Copy link
Copy Markdown
Member

@haileyok haileyok commented May 13, 2026

Fixes #260; related to #231

Summary

Migrates osprey_ui/ from npm to pnpm 10 via Corepack. Pure tooling swap — no application code changed, no React/Antd/Highcharts version bumps. 1,417 of 1,418 resolved transitives match the original npm baseline exactly; the single drop is a duplicate type def that npm's flat hoisting had silently kept alive.

Related Issues/Tasks

N/A

Changes Made

  • Phase 1 — Corepack pin + .npmrc swap. Added packageManager: "pnpm@10.33.4+sha512.…" to osprey_ui/package.json; replaced legacy-peer-deps=true with auto-install-peers=true + strict-peer-dependencies=false in osprey_ui/.npmrc.
  • Phase 2 — Lockfile parity. Generated osprey_ui/pnpm-lock.yaml via pnpm import; iteratively pinned 262 transitives in pnpm.overrides across 9 rounds until the parity harness reported zero drift against the npm baseline. Once the lockfile was committed, the overrides became redundant with the lockfile itself — dropped the entire pnpm.overrides block in a follow-up. Deleted osprey_ui/package-lock.json.
  • Phase 3 — Dockerfile. Switched osprey_ui/Dockerfile to the Corepack-on-Alpine pattern (npm install -g corepack@latest && corepack enable, then pnpm install --frozen-lockfile, CMD ["pnpm", "start"]).
  • Phase 4 — CI workflow. Migrated ui-quality in .github/workflows/code-quality.yml to pnpm/action-setup@v6 + actions/setup-node@v4 with cache: pnpm and cache-dependency-path: osprey_ui/pnpm-lock.yaml. Python and Rust jobs untouched.
  • Phase 5 — Docs. Swapped npm references for pnpm across AGENTS.md (7 sections), docs/DEVELOPMENT.md, docs/UI.md, osprey_ui/README.md.
  • Phase 6 — node-linker=hoisted. Added node-linker=hoisted to osprey_ui/.npmrc after discovering that pnpm's default isolated layout exposed two distinct cytoscape module identities to TypeScript (bundled types in cytoscape@3.33.1 vs @types/cytoscape@3.21.9 pulled in transitively by @types/cytoscape-dagre), breaking pnpm run build. Verified npm-based build at the base commit also installed both packages but npm's flat hoisting silently hid one — so the hoisted linker is the minimum-change restoration of build parity. Layout-only fix; resolved versions unchanged.

Deliberate deviation from design — pinned pnpm 10, not pnpm latest

The design plan said corepack use pnpm@latest. Today pnpm@latest is 11.1.1, which drops Node 18 support and moves .npmrc settings to pnpm-workspace.yaml. Both conflict with the design's DoD ("replacing the Dockerfile base image" is explicitly out of scope) and AC3 (.npmrc carries the peer-dep flags). Pinned pnpm 10.33.4 instead — the latest version that satisfies the existing constraints. If you want the Node 22 + pnpm 11 path, that's a separate scope change to the design.

Parity review checklist

  • pnpm-lock.yaml is the parity contract and the new source of truth. 1,417 of 1,418 resolved transitives match the original npm baseline exactly (the one drop: @types/express-serve-static-core@5.0.7 was a duplicate type def kept alive only by a parent-scoped override; the 4.19.6 that @types/express actually uses is still there). CI and Docker use pnpm install --frozen-lockfile, which refuses to update the lockfile.
  • node-linker=hoisted gives an npm-compatible flat node_modules layout. Single-package repo, so the isolation guarantee we give up matters less than build parity.
  • Future pnpm add or non-frozen pnpm install can re-resolve transitives within ^ ranges; the pnpm-lock.yaml diff in the PR is the review surface for catching unintended drift.

Confidence Level

Confidence Level: Claude

Testing

Verified locally on hailey.coder:

  • corepack pnpm install --frozen-lockfile from a clean node_modules exits 0 (Done in 2.6s using pnpm v10.33.4).
  • corepack pnpm run format:check exits 0 — "All matched files use Prettier code style!"
  • corepack pnpm run build exits 0 — build/index.html present, "Compiled successfully".
  • corepack pnpm start boots the dev server on :5002; curl http://127.0.0.1:5002 returns HTTP 200.
  • Diff harness (Phase 2) reported zero drift between the original npm baseline and the final pnpm-resolved lockfile; re-verified at HEAD after the overrides drop — 1,417/1,418 entries match.
  • Pre-existing npm run build at base commit 13e62d7 was verified to succeed in a temp worktree before declaring the node-linker fix necessary.

Deferred to operator / this PR's CI

  • docker compose build osprey-ui and docker compose up osprey-ui (AC4.2 / AC4.3) — Dockerfile is on AGENTS.md's human-approval list; image build deferred to local docker run or to CI/staging.
  • Playwright screenshot of the rendered UI (AC7.2 visual half).
  • CI green on this PR (AC7.4 — by definition only verifiable here).

A human test plan with step-by-step instructions for each deferred item lives at docs/test-plans/2026-05-13-pnpm-migration.md (untracked, per planning-docs policy).

Rollback

git revert the merge commit (or the squash). Devs would then need rm -rf osprey_ui/node_modules && npm ci to re-bootstrap. Single squash → clean revert.

Out of scope

Per the design DoD: no CRA / react-scripts upgrade, no React/Antd/Highcharts version bumps, no workspaces, no dependabot ecosystem changes, no base-image change.

haileyok and others added 13 commits May 13, 2026 08:28
Phase 1 of pnpm migration. Adds the packageManager field so Corepack
materializes pnpm 10.x identically across local dev, CI, and Docker.

Pinning pnpm 10 (not 11) because pnpm 11 drops Node 18 and moves
peer-dep config out of .npmrc — both conflict with this migration's
scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace legacy-peer-deps=true (npm) with auto-install-peers=true
and strict-peer-dependencies=false (pnpm equivalent). Preserves
the permissive peer-dep behavior CRA's outdated peer ranges
require.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each entry in pnpm.overrides represents a transitive whose
pnpm-resolved version drifted from the original npm package-lock.json
resolution. Pinning to the npm baseline so this migration is a pure
tooling swap with no transitive version bumps. Drift identified by
osprey_ui/scripts/lockfile_parity.py (committed here) and driven to
zero across N override rounds.

The harness lives in the tree through Phase 6's final re-verification
and gets deleted at the end of Phase 6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Strict-parity has been achieved via pnpm.overrides — every transitive
resolves to the same version as npm did. The npm lockfile is no longer
authoritative.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final lockfile after the strict-parity override loop. pnpm
resolves every transitive to the same version as the previous
npm package-lock.json.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 3 of pnpm migration. Replaces `npm install --legacy-peer-deps`
with the Corepack-on-Alpine pattern: upgrade bundled Corepack to dodge
the 2025 npm registry signature-key rotation, then `pnpm install
--frozen-lockfile` reads the lockfile committed in Phase 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 4 of pnpm migration. The ui-quality job now uses
pnpm/action-setup@v6 (ordered before actions/setup-node@v4) and
pnpm install --frozen-lockfile / pnpm run format:check. setup-node
caches the pnpm store keyed off osprey_ui/pnpm-lock.yaml.

Python and Rust jobs in the same workflow are untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 5 of pnpm migration. Updates AGENTS.md (Build, Testing, CI,
Dependencies, Human-approval-required sections), docs/DEVELOPMENT.md
(prerequisites), docs/UI.md (dev setup snippets), and
osprey_ui/README.md (prerequisites and dev commands) to reference
pnpm via Corepack.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pnpm's default isolated layout creates two distinct module identities
for cytoscape: the bundled types in cytoscape@3.33.1 and @types/cytoscape@3.21.9
pulled in transitively by @types/cytoscape-dagre and @types/cytoscape-popper.
TypeScript then sees two incompatible Ext / CytoscapeOptions definitions and
the production build fails. Under npm flat hoisting this is silently resolved.

node-linker=hoisted gives pnpm an npm-compatible flat node_modules without
giving up Corepack-pinned pnpm, the frozen lockfile, or the strict-parity
override contract. Single-package repo so isolation guarantees matter less
than build parity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The lockfile_parity.py harness served its purpose during the
Phase 2 strict-parity loop and the Phase 6 final re-verification.
Removing it now keeps the migration self-contained — future
dependency changes are reviewed via the pnpm.overrides block in
package.json, not by re-running a one-shot diff tool.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A future engineer pruning .npmrc would otherwise have to git blame the
line to recover the cytoscape duplicate-types context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 262-entry pnpm.overrides block was needed during the initial
strict-parity convergence in Phase 2. Once pnpm-lock.yaml was committed,
the lockfile itself locks every resolved version — the overrides were
belt-and-suspenders, not a separate enforcement mechanism. CI and Docker
already use `pnpm install --frozen-lockfile`, which reads the lockfile
verbatim and refuses to update it.

One incidental drift: `@types/express-serve-static-core@5.0.7` no longer
gets installed as a duplicate alongside `4.19.6`. It only existed under
npm/flat hoisting via a parent-scoped pin (`@types/express > @types/...`)
and is dead weight — `@types/express` actually needs 4.19.6, not 5.0.7.
All other 1,417 resolved name@version pairs match the original npm
baseline exactly.

Trade-off: a future `pnpm add some-pkg` or non-frozen `pnpm install` can
now re-resolve transitives within their `^` ranges. Mitigation lives in
the workflow — direct-dep changes go through review and surface in the
pnpm-lock.yaml diff there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The dependencies-management callout previously pointed reviewers at
the overrides block as the parity-enforcement mechanism. With overrides
gone, the lockfile is the contract — reviewers should check the
pnpm-lock.yaml diff on each pnpm add.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@haileyok haileyok marked this pull request as ready for review May 13, 2026 20:48
@haileyok haileyok requested review from a team, EXBreder, ayubun and vinaysrao1 as code owners May 13, 2026 20:48
Comment thread docs/DEVELOPMENT.md
- **[Git](https://git-scm.com/)** for version control
- **[uv](https://docs.astral.sh/uv/)** for Python package management
- **[npm](https://nodejs.org/en/download)**
- **[Node.js](https://nodejs.org/en/download/) 18+** for the UI (Corepack ships with Node and auto-resolves pnpm from `osprey_ui/package.json`'s `packageManager` field — no separate pnpm install needed)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is corepack what we want? im unfamiliar with it, though it does seem useful from my brief understanding

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "18"
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should probably update this at some point, but that can happen later

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate from npm to pnpm

1 participant