Skip to content

0025 — LTS / stable deps only, no bleeding-edge

0025 — LTS / stable deps only, no bleeding-edge

Section titled “0025 — LTS / stable deps only, no bleeding-edge”

Status: Accepted Date: 2026-04-21 Related: project_lts_deps_only memory

By 2026-04-21 the Ideony monorepo has ~280 runtime deps and ~90 dev deps across apps/{api,mobile} + packages/*. At this scale a single mis-pinned transitive can take the whole app down — and has, twice this week:

  1. pretty-format@30.3.0 crash (this commit) — Expo’s @expo/metro-runtime@5.0.4 HMRClient calls require("pretty-format") at bundle time with no declared dep. pnpm hoisted the 30.3.0 copy pulled in by @testing-library/react-native@13.3.3. v30 ships build/index.mjs with export default cjsModule.default, which — once Metro’s babel transform rewrites it — evaluates to undefined because _interopDefault leaves the CJS exports object untouched (it has __esModule: true), so cjsModule.default is the format function and .default.default does not exist. Every web page blanked with TypeError: Cannot read properties of undefined (reading 'default') before React could mount. Fixed by Metro resolver redirect to the v29.7.0 CJS-only copy that Expo itself links.

  2. expo-router@5.0.7 vs @expo/router-server@55.0.14 skew — router-server expects expo-router/internal/routing from expo-router@55.0.12, but Expo SDK 55 pins the 5.0.7 release. Workaround: experiments.typedRoutes: false in apps/mobile/app.json. Real fix (post-demo) is to align the two versions.

Both bugs came from non-LTS / mismatched-major versions propagating into the workspace.

Every dep added or upgraded in Ideony MUST be on a stable / LTS / GA channel. No next-tag, canary, beta, or rc releases, even when the latest stable has a known bug.

Pins (current state, enforced via lockfile + PR review):

  • Node.js — 22.x LTS (engines.node >=22.0.0)
  • Expo SDK — 55 stable
  • React / React Native — whatever Expo SDK 55 declares as peer; never force-upgrade above
  • NestJS — 11 stable
  • Prisma — 7.x stable
  • Test libs (Jest / Vitest / Playwright / @testing-library/*) — minor-pin; reject major bumps that pull breaking transitives
  • pnpm 9.15.4, Turbo 2.x, Biome 2.x — stable minor

Pre-upgrade procedure (mandatory):

  1. Fetch docs via Context7 MCP — confirm version is tagged stable / GA / LTS (never next, canary, beta, rc).
  2. Cross-check Exa MCP for open CVEs / migration warnings on that version.
  3. Inspect peerDependencies — reject if it pulls a canary or breaking transitive.
  4. If a peer conflict forces a non-LTS, add pnpm.overrides in root package.json and cite this ADR in the override comment — never silently accept the hoisted non-LTS version.
  5. If a dep ships an ESM-only .mjs Metro cannot digest, add a resolver redirect in apps/mobile/metro.config.js to the last CJS-only version rather than accept the runtime break.

Major upgrades: only post-launch, only via a new ADR that documents migration + risk, only with full Docker + E2E verification pass before merge.

Positive

  • Predictable bug surface for the remaining pre-launch push.
  • No more “the lockfile silently pulled a broken transitive” post-mortems.
  • Metro resolver redirects become a documented workaround pattern rather than ad-hoc patches.

Negative

  • Some test libs will lag behind the latest features for a release cycle (accepted — we prioritize shipping over cutting-edge testing tooling).
  • pnpm.overrides block in root package.json will grow — every entry must link back to an ADR.

Neutral

  • Security patches still land on schedule (stable-channel updates include CVE fixes); this policy blocks feature-chasing, not security patches.
  • “Upgrade always, fix breakage when it happens.” Rejected — this is the status quo that produced the two bugs cited in Context. Bug budget pre-launch is zero.
  • “Pin lockfile manually per dep, no policy.” Rejected — a policy is cheaper than a per-upgrade argument, and a new dev (or LLM agent) needs written guidance.
  • “Auto-update bot (Renovate / Dependabot).” Deferred post-launch. Premature in MVP 0 where a single bad minor bump blocks a demo.
  • This ADR lands alongside the pretty-format@30 Metro resolver redirect.
  • CLAUDE.md “Dependency policy” section added as top-level rule.
  • Memory file feedback_lts_deps_only.md added to session memory so future agents apply the rule without re-asking.
  • No immediate lockfile changes — existing pinned versions already satisfy the policy; the only outlier is pretty-format@30.3.0 which stays installed for @testing-library/react-native@13.3.3 tests but is diverted at the Metro bundle level on web.