Skip to content

DLS-997 chore(deps): add syncpack to enforce one version per package - #940

Merged
gamegee merged 19 commits into
mainfrom
chore/syncpack
Oct 28, 2026
Merged

gamegee merged 19 commits into
mainfrom
chore/syncpack

Conversation

@gamegee

@gamegee gamegee commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Context

Our 11 package.json files declared the same packages at different versions, and nothing caught it. For example, two Storybook versions were installed side by side. This PR enforces one version per package across dependencies, devDependencies and peers. A second version needs an explicit, labeled exception.

No published package changes. The libs/* manifests are identical to main, so there is no version plan and no release.

Closes DLS-997

Technical details

New checks (CI job dependency-alignment)

Check Command Catches
syncpack npm run check:deps the same package at two versions in different manifests
Peer validator npm run check:peer-deps a lib peer we don't test with, or with no upper bound
@nx/dependency-checks lint a lib importing something it doesn't declare (now also on ui-react, ui-rnative and design-core)

Fix drift with npm run fix:deps, which also refreshes the lockfile.

Config: .syncpackrc.json

  • Packages released together count as one: nx, Storybook (web), Storybook (on-device), react, react-native, vitest, tailwind, svgr.
  • Published libs' dependencies must use ^.
  • Two labeled exceptions:
    • ui-rnative peers on React 19 only.
    • The sandbox apps use * for the local libs.
  • Banned packages, so they can't come back: @storybook/jest, @storybook/testing-library, @storybook/addon-react-native-web.

The peer validator lists the two ui-rnative peers already published without an upper bound (react-native-gesture-handler, expo), each with a reason. An entry fails once its range is bounded.

Cleanup

  • One Storybook version (10.2.14) instead of two.
  • The RN sandbox's prettier, jest, @types/jest and @babel/core match the root.
  • Removed unused packages:
    • @storybook/test-runner: nothing ran it, and Chromatic already renders every story.
    • @react-native/eslint-config.
    • 3 deprecated Storybook packages.
    • The jest 30 utilities that sat next to jest 29.
  • Result: 364 fewer packages in the lockfile (3,230 → 2,866), and npm ls reports no invalid peer.

Maintenance

  • internals/repo-tools is split into one folder per use case, named after its npm script or workflow, grouped under ci/: check-agent-docs/, check-package-exports/, check-peer-deps/, fix-lcov-paths/, publish-dev-packages/. Helpers sit in shared/.
  • @nx/dependency-checks config is shared through defineDependencyChecks() in eslint.shared.mjs. Each lib passes only its own exceptions, with their reasons.
  • Node version: CI reads it from .mise.toml instead of hardcoding it in two more places.
  • New dependency-management skill: where a package goes, which range to write, how to add an exception, and how to fix each failing check. pr-review routes package.json changes to it.

Not in this PR

  • Narrowing peer ranges (adding upper bounds, raising minimums) can break consumers, so it waits for a planned release.
  • Exact-pin enforcement: syncpack can't read the lockfile, so it would downgrade versions to the range minimum.
  • Dropping legacy-peer-deps: no peer conflict is left, but npm would then install missing peers, including detox (about 318 packages). Separate PR.

How to test

  1. npm ci, then npm run check:deps and npm run check:peer-deps: both pass.
  2. Change a version in one manifest, or narrow a peer (e.g. react-native-svg peer → ^14.0.0): the matching check fails.

🤖 Generated with Claude Code

- .syncpackrc.json: one version per package across all manifests, identical
  peer ranges across published libs, lockstep families (nx, storybook web,
  react, react-native, vitest, tailwind, svgr) treated as one package, and
  explicit labeled exceptions (ui-rnative react ^19 peer, sandbox `*`).
- repo-tools peerDeps validator: every lib peer is installed in the root
  devDependencies at a version its range accepts.
- New `dependency-alignment` CI job running both checks.
- Align existing drift: storybook web family on 10.2.14 (10.2.13 and 10.2.14
  were both installed), sandbox prettier/jest/@types/jest/@babel/core,
  ui-rnative @types/react, drop unused jest-util/jest-environment-node 30,
  add expo/expo-haptics peers to root devDependencies.
@gamegee
gamegee requested a review from a team as a code owner October 26, 2026 22:29
Copilot AI balanced review requested due to automatic review settings October 26, 2026 22:29
@vercel

vercel Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
ldls Ready Ready Preview Oct 28, 2026 4:03pm UTC
ldls-react-native Ready Ready Preview Oct 28, 2026 4:03pm UTC

Request Review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The peer validator checks declared range minima rather than installed versions, allowing incompatible dependencies to pass.

1 open issue
What changed in this PR

This PR adds dependency-alignment checks across Lumen’s workspace manifests to prevent version drift in shared tooling and published libraries.

Changes:

  • Configures syncpack with package-family alignment and explicit exceptions.
  • Adds a peer-dependency validator and CI checks.
  • Aligns existing dependency declarations and updates the lockfile, documentation, and React Native version plan.
File Description
package.json Adds validation commands and tools; aligns shared dependencies.
package-lock.json Records aligned versions and updated dependency resolution.
libs/​ui-rnative/​package.json Aligns the React types development dependency.
internals/​repo-tools/​src/​ci/​validators/​peerDeps.mjs Adds library peer compatibility checks.
internals/​repo-tools/​README.md Documents the validator and its CI job.
apps/​app-sandbox-rnative/​package.json Aligns Jest, Jest types, and Prettier.
AGENTS.md Documents dependency-alignment commands and exceptions.
.syncpackrc.json Defines version alignment, package families, and range rules.
.nx/​version-plans/​version-plan-1793033726214-ui-rnative.md Adds a React Native package patch plan.
.github/​workflows/​pr.yml Runs dependency-alignment and peer checks in CI.

Review effort: Balanced

The root range floor can sit inside a peer range while the version CI
installs does not (e.g. root ~5.6.0 installs 5.6.2, peer 5.6.0), so read
the installed version from package-lock.json before calling satisfies.
… design-core

Stories, tests, Figma Code Connect, Storybook and tool configs are ignored, as
is the SVGR icon template that only internals/sync-figma codegen reads.

Remaining entries, decided per lib:
- @types/d3-scale stays a dependency: the public NumericScale/CategoricalScale
  types in the emitted .d.ts import from d3-scale, which ships no types.
- @types/d3-shape moves to root devDependencies: no emitted .d.ts references it.
- ui-rnative keeps the @types/react peer (public props use React 19 ref-as-prop)
  and the expo peer (expo-haptics peers on it; v15 needs SDK 54+).

The rule is scoped to package.json: **/*.json would also put the unformatted
tsconfig/figma JSON files under prettier.
…ettier

ui-rnative peers now stop at the tested major, with floors at the tested minor
and never above what Ledger Live mobile pins today:
- expo >=54.0.0 -> ^54.0.0 (expo-haptics ~15.0.8 only supports SDK 54)
- react-native-gesture-handler >=2.0.0 -> ^2.28.0
- react-native-safe-area-context ^4 || ^5 -> ^5.6.0 (v4 was never installed)
- react-native-svg ^15.0.0 -> ^15.12.0
- @gorhom/bottom-sheet ^5.0.0 -> ^5.2.0
@types/react stays ^19.0.0: Ledger Live is on 19.0.14.

The peer validator now rejects ranges with no upper bound.

@storybook/jest, @storybook/testing-library and
@storybook/addon-react-native-web are referenced nowhere; prettier is pinned to
3.8.3 because its output changes between minors.
ui-react and ui-rnative ship the same dependencies and peers as on main, so
this PR needs no release. The root devDependencies adapt instead: @types/react
takes the lib's ^19.0.0 range (the lockfile still installs 19.1.17), and
@types/d3-shape stays a lib dependency, ignored by @nx/dependency-checks.

The peer validator keeps its no-upper-bound guard, with the two ui-rnative
peers already published unbounded listed as explicit exceptions.
Scripts were split by kind (ci/, ci/validators/, lib/), so finding the code
behind a CI job meant knowing how it was classified. Each folder is now named
after the npm script or workflow that runs it: check-agent-docs,
check-package-exports, check-peer-deps, fix-lcov-paths and
publish-dev-packages, with shared helpers in shared/.

The README also had check:package-exports in the static-analysis job; it runs
in bundle-checks.
Moves the per-peer decision into checkPeer() so it can be tested without a
workspace on disk, like fixLcovPaths. The tests pin each failure: missing
from the root devDependencies, only installed nested, installed outside the
range, and no upper bound.

An unbounded-peer exception now fails once the range has an upper bound, so
the list can't keep names after the peer is fixed.
@storybook/react-native and the addon-ondevice-* packages release together
but were not grouped, so bumping one of them alone passed the check.

@storybook/jest, @storybook/testing-library and
@storybook/addon-react-native-web were removed from the root as unused.
Banning them stops them coming back.
syncpack fix rewrites the manifests but not package-lock.json, so npm ci then
failed in CI until someone ran npm install by hand.
A glob with no wildcard (package.json) is its own static prefix, so the
drift check tried to walk it as a folder and reported a dead trigger for a
file that exists.
Where a package belongs (root devDependencies, a lib's dependencies, or a
peer), which range to write, how to declare an exception and how to fix each
check:deps / check:peer-deps / @nx/dependency-checks failure. Its Review
checks table replaces the one-line dependency tip in pr-review, which now
routes package.json changes to it.

release-plan now says a lib's package.json is not exempt from plan:check;
it only mentioned libs/*/src/.
Entries were bare package names. Each is now { name, reason }, the same way
every syncpack exception carries a label, and a test fails on an empty
reason.
The sandbox lints with the workspace's shared config; nothing imports this
preset. It brought ESLint 8 era plugins whose peers our ESLint 9 and
@typescript-eslint 8 failed: 36 packages fewer, 2 of the 3 invalid peers
gone.
Nx inferred a test-storybook target from it, but no CI job, script or doc
ran it, Playwright was never installed for it, and Chromatic already renders
every story and runs its play functions. Storybook 10 points Vite projects to
@storybook/addon-vitest for story tests.

It shipped its own jest 30 next to our jest 29: 173 packages fewer, one jest,
and no invalid peer left in npm ls.
22.23.4 was set in .mise.toml, as the setup action's default, and again in
the agent-drift job. The action said it defaulted to .mise.toml but
hardcoded the value. It now reads it from there, and agent-drift uses the
action with install off, so a Node bump edits one file.
…libs

Each published lib repeated the same block: files, parser, severity and the
dev-only files to skip. defineDependencyChecks() in eslint.shared.mjs now
holds that, and a lib passes only its own extra files and ignored
dependencies, with their reasons. A helper rather than a prodConfig entry
because ESLint replaces rule options instead of merging them.

utils-shared and lint-plugin now match package.json like the others instead
of **/*.json; their other JSON files were already prettier-clean.
Every use-case folder runs in CI, so they now sit under ci/, leaving room for
other groups (local codegen, release helpers) next to it. shared/ stays at
the top since any group may import it.

@zel-kass zel-kass left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

lgtm

Comment thread .github/actions/setup/action.yml Resolved
@sonarqubecloud

Copy link
Copy Markdown

@gamegee
gamegee merged commit 1a7d23c into main Oct 28, 2026
25 checks passed
@gamegee
gamegee deleted the chore/syncpack branch October 28, 2026 22:02

This branch was successfully deployed

2 active deployments
Preview – ldls — 7055dbf3 Deployed Oct 28, 2026 by vercel[bot]
Preview – ldls-react-native — 7055dbf3 Deployed Oct 28, 2026 by vercel[bot]
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.

4 participants