Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2d12d3f
chore(deps): add syncpack to enforce one version per package
gamegee Oct 26, 2026
bbe10a1
fix(repo-tools): check peers against the lockfile-installed version
gamegee Oct 26, 2026
29844d6
chore(lint): enable @nx/dependency-checks on ui-react, ui-rnative and…
gamegee Oct 26, 2026
6383b5b
chore(deps): bound ui-rnative peers, drop dead Storybook deps, pin pr…
gamegee Oct 27, 2026
af2e7a6
chore(deps): keep published lib manifests unchanged
gamegee Oct 27, 2026
47d6b77
refactor(repo-tools): one folder per use case
gamegee Oct 27, 2026
673e029
test(repo-tools): cover the peer dependency check
gamegee Oct 27, 2026
0e64f52
chore(syncpack): lock the on-device Storybook family, ban dead packages
gamegee Oct 27, 2026
0484075
chore(deps): refresh the lockfile in fix:deps
gamegee Oct 27, 2026
4694fbc
fix(repo-tools): accept literal file paths in skill paths globs
gamegee Oct 27, 2026
a20e751
docs(skills): add dependency-management
gamegee Oct 27, 2026
d71321c
chore(repo-tools): give each unbounded-peer exception a reason
gamegee Oct 27, 2026
06b607e
chore(sandbox-rnative): drop unused @react-native/eslint-config
gamegee Oct 27, 2026
bf05e44
chore(deps): drop unused @storybook/test-runner
gamegee Oct 27, 2026
9803088
ci: read the Node version from .mise.toml
gamegee Oct 27, 2026
2c54add
chore(lint): share the @nx/dependency-checks config across published …
gamegee Oct 27, 2026
930d0a8
refactor(repo-tools): group the CI scripts under src/ci/
gamegee Oct 27, 2026
82a6bdb
chore: trim redundant comments
gamegee Oct 28, 2026
7055dbf
ci(setup): drop the redundant empty node-version default
gamegee Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions .claude/skills/dependency-management/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
name: dependency-management
description: >-
Use when adding, removing, upgrading or moving an npm dependency in any
package.json of the workspace, or when `check:deps`, `check:peer-deps` or
`@nx/dependency-checks` fails — where a package belongs (root
devDependencies, a lib's dependencies, or a peer), which range to write, how
to allow a second version, and when a version plan is needed.
paths: package.json, libs/*/package.json, apps/*/package.json, internals/*/package.json, .syncpackrc.json
---

# Dependency management

One version per package across every manifest. The rules are enforced by
`.syncpackrc.json` (`npm run check:deps`), the peer validator
(`npm run check:peer-deps`) and `@nx/dependency-checks` in each lib's
`eslint.config.mjs`. This skill says where a package goes and what to run.

## Where a package goes

| The package is… | Declare it in | Range |
| --- | --- | --- |
| Build, test, lint or type tooling | root `devDependencies` | match the neighbors; the lockfile pins the version |
| Imported by a published lib's shipped code | that lib's `dependencies` only | `^` (enforced), so consumers can dedupe |
| Provided once by the consumer app (React, React Native, Radix, `@ledgerhq/lumen-design-core`) | the lib's `peerDependencies` **and** root `devDependencies` | peer: a range with an upper bound; root: the version we test with |
| Used only by a sandbox app | that app's `package.json` | the same specifier as everywhere else |
| Used only by an `internals/*` project | root `devDependencies` (see `## Internals` in `AGENTS.md`) | as tooling |

- `@ledgerhq/lumen-*` versions between libs are written by `nx release`. Never
edit them by hand. Sandbox apps use `*`.
- A types-only package (`@types/*`) goes in a lib's `dependencies` only when the
emitted `.d.ts` imports it; list it in that lib's `ignoredDependencies` for
`@nx/dependency-checks`, with a comment saying why.

## Adding or upgrading

1. Install where it belongs: `npm install -D <pkg>@<version>` at the root, or
`npm install <pkg>@<version> -w libs/<lib>`. `.npmrc` already sets
`legacy-peer-deps`.
2. Upgrade a family in one step. The families are the `dependencyGroups` in
`.syncpackrc.json` (nx, Storybook web, Storybook on-device, react,
react-native, vitest, tailwind, svgr). Nx goes through `nx migrate` (see the
`nx` skill).
3. `npm run fix:deps` aligns the other manifests and refreshes the lockfile.
4. `npm run check:deps && npm run check:peer-deps`, then lint the touched libs.
5. Any edit to a `libs/*/package.json` needs a version plan (see the
`release-plan` skill).

Changing a lib's `dependencies` or `peerDependencies` reaches consumers. Widening
a peer range is safe; raising its floor or adding an upper bound can break a
consumer, so it ships in a planned release, not a dependency chore.

## Allowing an exception

Never loosen a rule for everyone. Each exception names its package and says why:

- **A second version**: a `versionGroups` entry at the top of `.syncpackrc.json`
(the first matching group wins) with a `label` giving the reason.
- **A package that must not come back**: a `versionGroups` entry with
`isBanned: true` and a `label`.
- **A peer with no upper bound**: a `{ name, reason }` entry in
`unboundedPeerExceptions` in
`internals/repo-tools/src/ci/check-peer-deps/peerDeps.mjs`. The check fails once
the range is bounded, so remove the entry then.

## When a check fails

| Message | Fix |
| --- | --- |
| `check:deps`: `DiffersToHighestOrLowestSemver`, `SemverRangeMismatch` | `npm run fix:deps` |
| `check:deps`: `IsBanned` | remove the package; its `label` names the replacement |
| `check:peer-deps`: not in the root devDependencies | add it to the root at the version you test with |
| `check:peer-deps`: installed … is outside peer … | move the root version into the range; widening the peer is a lib change |
| `check:peer-deps`: has no upper bound | bound the range, or add an explained exception |
| `@nx/dependency-checks`: missing / not used | declare what the shipped code imports; remove what it doesn't |

## Review checks

Rules verifiable from a diff.

| Check | Applies to | Detect | Skip |
| --- | --- | --- | --- |
| Tooling in a published lib's `dependencies` | libs | test, build or lint package under `dependencies` | — |
| Peer without its root devDependency | libs | new `peerDependencies` key absent from root `devDependencies` | `@ledgerhq/lumen-*` |
| Peer narrowed in a dependency chore | libs | peer floor raised or upper bound added, PR not framed as a release change | — |
| Unexplained exception | `.syncpackrc.json`, `peerDeps.mjs` | new `versionGroups` entry without a `label`, or exception without a comment | — |
| Lib manifest change without a version plan | libs | `libs/*/package.json` changed, no matching `.nx/version-plans/` file | — |
| Lockfile not refreshed | all | `package.json` dependency changed, `package-lock.json` untouched | — |
2 changes: 1 addition & 1 deletion .claude/skills/pr-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ skill's `## Review checks` table — this skill does **not** restate them:
| Stories | `component-stories` |
| MDX docs | `component-mdx` |
| Code Connect | `code-connect` |
| Dependencies (any `package.json`, `.syncpackrc.json`) | `dependency-management` |
| Version plan | `release-plan` (scored below) |

Rules:
Expand Down Expand Up @@ -244,4 +245,3 @@ lib, marking each item pass/fail]
- Compare against a similar existing component in the **same** library for
pattern alignment.
- Check the PR doesn't accidentally remove or break existing barrel exports.
- Verify new dependencies land in the correct `package.json` (peer vs dev).
3 changes: 2 additions & 1 deletion .claude/skills/release-plan/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ are defined authoritatively in `nx.json` under
`release.versionPlans.ignorePatternsForPlanCheck`: `*.test.*` / `*.spec.*`,
`*.stories.*`, `*.md`, `*.mdx`, `*.figma.@(ts|tsx)`, `tsconfig*.json`, eslint
configs, `jest.config.*`, `test-setup.*`, `*.snap`, `.storybook/**`. A PR
touching only these needs no plan.
touching only these needs no plan. A lib's `package.json` is not exempt: any
edit to it, devDependencies included, needs a plan.

`internals/*` is outside `release.projects`, so a PR touching only `internals/*`
never needs a plan.
17 changes: 15 additions & 2 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ inputs:
node-version:
description: Node.js version to install. Defaults to the version pinned in `.mise.toml`.
required: false
default: '22.23.4'
default: ''
Comment thread
gamegee marked this conversation as resolved.
Resolved
install:
description: Whether to run `npm ci --legacy-peer-deps`.
required: false
Expand All @@ -29,10 +29,23 @@ inputs:
runs:
using: composite
steps:
# `.mise.toml` is what developers run locally, so CI reads it rather than
# repeating the version (setup-node can't parse it).
- name: Read Node.js version from .mise.toml
id: mise
shell: bash
run: |
version=$(sed -n 's/^node *= *"\(.*\)"/\1/p' .mise.toml)
if [ -z "$version" ]; then
echo "::error file=.mise.toml::no node version found"
exit 1
fi
echo "node=$version" >> "$GITHUB_OUTPUT"

- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: ${{ inputs.node-version }}
node-version: ${{ inputs.node-version || steps.mise.outputs.node }}
cache: 'npm'
cache-dependency-path: '**/package-lock.json'

Expand Down
29 changes: 25 additions & 4 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,35 @@ jobs:

# Zero-dependency Node script (AGENTS.md ↔ .claude/skills ↔ codebase);
# no npm install or Nx needed, so keep this job lean.
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
- uses: ./.github/actions/setup
with:
node-version: '22.23.4'
install: 'false'
cache-nx: 'false'
set-shas: 'false'

- name: Check agent docs & structure for drift
run: npm run check:agent-docs

dependency-alignment:
name: Dependency alignment
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- uses: ./.github/actions/setup
with:
cache-nx: 'false'
set-shas: 'false'

- name: One version per package across manifests (syncpack)
run: npm run check:deps

- name: Peer ranges accept the versions we test with
run: npm run check:peer-deps

static-analysis:
name: Static Analysis
runs-on: ubuntu-latest
Expand Down Expand Up @@ -126,7 +147,7 @@ jobs:
fi

- name: Fix lcov paths to be relative to workspace root
run: node internals/repo-tools/src/ci/fixLcovPaths.mjs
run: node internals/repo-tools/src/ci/fix-lcov-paths/fixLcovPaths.mjs

- name: Upload coverage reports
uses: actions/upload-artifact@v4
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish-dev-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
- name: Determine affected libraries
id: affected
run: |
PUBLISHABLE=$(node internals/repo-tools/src/ci/getPublishableProjects.mjs | paste -sd, -)
PUBLISHABLE=$(node internals/repo-tools/src/ci/publish-dev-packages/getPublishableProjects.mjs | paste -sd, -)
AFFECTED=$(npx nx show projects --affected --projects="$PUBLISHABLE" 2>/dev/null || true)
if [ -z "$AFFECTED" ]; then
echo "No affected libraries found"
Expand All @@ -84,7 +84,7 @@ jobs:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
REPO: ${{ github.repository }}
AFFECTED_PROJECTS: ${{ steps.affected.outputs.projects }}
run: node internals/repo-tools/src/ci/generateTarballs.mjs
run: node internals/repo-tools/src/ci/publish-dev-packages/generateTarballs.mjs

- name: Publish tarballs to GitHub pre-release
if: steps.affected.outputs.has_affected == 'true'
Expand Down
110 changes: 110 additions & 0 deletions .syncpackrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
{
"$schema": "./node_modules/syncpack/schema.json",
"source": [
"package.json",
"apps/*/package.json",
"internals/*/package.json",
"libs/*/package.json"
],
"dependencyGroups": [
{
"aliasName": "nx (family)",
"dependencies": ["nx", "@nx/**"],
"dependencyTypes": ["dev", "prod"]
},
{
"aliasName": "storybook (web family)",
"dependencies": [
"storybook",
"@storybook/addon-a11y",
"@storybook/addon-docs",
"@storybook/addon-themes",
"@storybook/cli",
"@storybook/react-native-web-vite",
"@storybook/react-vite",
"eslint-plugin-storybook"
],
"dependencyTypes": ["dev", "prod"]
},
{
"aliasName": "storybook (on-device family)",
"dependencies": [
"@storybook/react-native",
"@storybook/addon-ondevice-*"
],
"dependencyTypes": ["dev", "prod"]
},
{
"aliasName": "react (family)",
"dependencies": ["react", "react-dom", "react-test-renderer"],
"dependencyTypes": ["dev", "prod"]
},
{
"aliasName": "react-native (family)",
"dependencies": ["react-native", "@react-native/**"],
"dependencyTypes": ["dev", "prod"]
},
{
"aliasName": "vitest (family)",
"dependencies": ["vitest", "@vitest/**"],
"dependencyTypes": ["dev", "prod"]
},
{
"aliasName": "tailwindcss (family)",
"dependencies": ["tailwindcss", "@tailwindcss/**"],
"dependencyTypes": ["dev", "prod"]
},
{
"aliasName": "svgr (family)",
"dependencies": ["@svgr/**"],
"dependencyTypes": ["dev", "prod"]
}
],
"versionGroups": [
{
"label": "Deprecated since Storybook 8: import from storybook/test instead",
"dependencies": ["@storybook/jest", "@storybook/testing-library"],
"isBanned": true
},
{
"label": "Superseded by the @storybook/react-native-web-vite framework",
"dependencies": ["@storybook/addon-react-native-web"],
"isBanned": true
},
{
"label": "Sandbox apps consume the local libs straight from the workspace",
"packages": ["app-sandbox-*"],
"dependencies": ["$LOCAL"],
"dependencyTypes": ["prod", "dev"],
"pinVersion": "*"
},
{
"label": "ui-rnative peers on React 19 only: React Native 0.81 requires it, the web libs still support 18",
"packages": ["@ledgerhq/lumen-ui-rnative"],
"dependencies": ["react"],
"dependencyTypes": ["peer"],
"pinVersion": "^19.0.0"
},
{
"label": "Peer ranges are identical across the published libs",
"dependencyTypes": ["peer"]
},
{
"label": "One version per package (or package family) across dependencies and devDependencies",
"dependencyTypes": ["!peer"]
}
],
"semverGroups": [
{
"label": "Local packages keep the exact version nx release writes",
"dependencies": ["$LOCAL"],
"isIgnored": true
},
{
"label": "Published libs use caret ranges so consumers can dedupe",
"packages": ["@ledgerhq/lumen-*"],
"dependencyTypes": ["prod"],
"range": "^"
}
]
}
12 changes: 8 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ Rules for adding one:
Filesystem _writes_ into libs are expected; only imports are governed.
- Run `npx nx sync` after adding one.

`internals/repo-tools/src/ci/validators/agentDocsDrift.mjs` enforces the table
and the first four rules.
`internals/repo-tools/src/ci/check-agent-docs/agentDocsDrift.mjs` enforces
the table and the first four rules.

## Commands

Expand All @@ -64,6 +64,9 @@ and the first four rules.
- Typecheck all: `npx nx run-many --target=typecheck --all`
- Build all: `npx nx run-many --target=build --all`
- Storybook: `npx nx run @ledgerhq/lumen-ui-react:serve:storybook`
- Dependency versions: `npm run check:deps` (fix with `npm run fix:deps`) — one
version per package across every manifest. A second version is allowed only
through a labeled `versionGroups` entry in `.syncpackrc.json`.

Prefer the Nx MCP server (`nx_workspace`, `nx_generators`, …) for workspace
questions and generators instead of guessing.
Expand Down Expand Up @@ -122,6 +125,7 @@ frontmatter controls auto-attach.
| implementing a component from a Figma link | `figma-to-code` |
| creating/updating Figma Code Connect (`*.figma.tsx`) | `code-connect` |
| syncing design tokens to React Native JS themes after a Figma sync | `figma-token-sync` |
| adding, upgrading or moving a dependency, or a failing `check:deps` | `dependency-management` |
| running Nx tasks/generators, or asking workspace questions | `nx` |

### Process
Expand Down Expand Up @@ -166,8 +170,8 @@ One source, no duplication:
consumer-facing names (e.g. `lumen-code`) free for the published tree.
- Per-tool files (`CLAUDE.md`, `.github/copilot-instructions.md`,
`.github/instructions/*`) are thin pointers/wrappers — they must not restate
content. The `internals/repo-tools/src/ci/validators/agentDocsDrift.mjs` drift
check (run in CI as the
content. The `internals/repo-tools/src/ci/check-agent-docs/agentDocsDrift.mjs`
drift check (run in CI as the
`agent-drift` job via `npm run check:agent-docs`) enforces the mechanical
invariants: the AGENTS.md index and the skill folders stay in bijection,
inter-skill references resolve, cited repo paths exist, no skill hardcodes a
Expand Down
9 changes: 4 additions & 5 deletions apps/app-sandbox-rnative/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,11 @@
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
"@react-native/babel-preset": "0.81.6",
"@react-native/eslint-config": "0.81.6",
"@react-native/typescript-config": "0.81.6",
"@types/jest": "^29.5.13",
"@types/jest": "29.5.14",
"babel-preset-expo": "~54.0.10",
"jest": "^29.6.3",
"prettier": "2.8.8"
"jest": "~29.7.0",
"prettier": "3.8.3"
},
"engines": {
"node": ">=18"
Expand All @@ -54,4 +53,4 @@
"scope:react-native"
]
}
}
}
Loading
Loading