Skip to content

fix(deps)!: move adapter peers onto the UIKit v3 line and accept Vite 8 - #86

Merged
pasevin merged 2 commits into
mainfrom
fix/uikit-v3-peers-and-vite8
Sep 9, 2026
Merged

fix(deps)!: move adapter peers onto the UIKit v3 line and accept Vite 8#86
pasevin merged 2 commits into
mainfrom
fix/uikit-v3-peers-and-vite8

Conversation

@pasevin

@pasevin pasevin commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

The defect

Two independent install blockers, both manifest-only.

1. Adapters could not be installed on current UIKit. Every adapter package declared UIKit
v2 for ui-components, ui-react and ui-utils while already declaring v3 for
ui-types. UIKit's current line is components 3.x, react 3.x, utils 4.x, types 3.5.x — so no
consumer on current UIKit could install any adapter without unmet peers. The manifests described
a combination that no longer exists.

2. adapters-vite could not be installed on Vite 8. Versions 8.0.0 through 12.0.0 all
declared vite: ^7.0.0, so no published version installs against Vite 8.

Why the ranges said v2 — this was not neglect

Worth stating, because the obvious reading is that these pins were left to rot. They were not.
They were correct when written, and a release-process bug on the UIKit side moved the goalposts
without changing a line of API.

The capability migration (openzeppelin-ui#113) was released as 2.0.0 across all four packages.
Its changeset files were then reintroduced by a later merge that restored the branch's copies of
files main had deleted at release, and the next release consumed them a second time as
3.0.0. One breaking change, two majors.

I verified this against the published CHANGELOGs rather than taking it on report. In
packages/react/CHANGELOG.md, the ## 2.0.0 and ## 3.0.0 major-change entries are both 1665
characters, both cite PR #113, and carry identical prose — they differ only in the commit SHA
(52f2823 vs 6f09f66). packages/components/CHANGELOG.md shows the same shape at 991
characters each. The same bug recurred later: components 3.7.0 and 3.8.0 are 907 characters
each, the first citing #193 and the second #195, the PR that reintroduced it.

So the adapters migrated to the capability API, which landed in 2.0.0, pinned ^2.0.0
correctly — and were then a full major behind an API that never changed again. That is precisely
why the file-level diff of every consumed source comes back byte-identical and why this PR needs
zero source changes: there was never anything to migrate. Meanwhile ui-types kept shipping real
minors the adapters needed, which dragged that one range forward to ^3.5.0 on its own. Hence
the half-migrated state in the table above, which looked coherent from the inside.

Credit for tracing this to the UIKit side. Two follow-ups are in flight there: a CI guard that
fails when a changeset already published in a CHANGELOG is still sitting in .changeset/
(openzeppelin-ui#230), and a oz-ui-dev check-peers warning when a declared range does not admit
the installed version (openzeppelin-ui#229). Once #229 lands, adding
pnpm exec oz-ui-dev check-peers --project "$PWD" to this repo's CI after install is the signal
that was missing here — run against this tree as it stands it passes clean across 20 peer pairs.

What changed

package ui-components ui-react ui-utils ui-types
all seven adapter packages ^2.0.0^3.9.0 ^2.0.1^3.3.2 ^2.0.0^4.0.1 ^3.2.0/^3.5.0^3.5.2

Plus @openzeppelin/adapters-vite: vite ^7.0.0^7.0.0 || ^8.0.0.

No source changes. The only files touched are the eight package.json manifests, the
lockfile, the ui-types dedupe override in pnpm-workspace.yaml, one test constant, and two
changesets.

Why the floors are exact patch versions and not ^3.0.0 / ^4.0.0

I started with major-level floors and tightened them after review from the UIKit side. The four
UIKit packages depend on each other with dependencies, not peers:

  • ui-components@3.9.0ui-utils ^4.0.1, ui-types ^3.5.2
  • ui-react@3.3.2ui-components ^3.8.3, ui-utils ^4.0.1, ui-types ^3.5.2
  • ui-utils@4.0.1ui-types ^3.5.2

So a floor below those lets a consumer satisfy our peer with an older patch while UIKit resolves
a second, nested copy for itself. That is not cosmetic:

  • Two copies of ui-utils split its module-level singletons. appConfigService and
    Logger.getInstance() hold init state. The adapter's copy would be permanently uninitialized
    and silently return defaults behind a "called before initialization" warning.
  • Two copies of ui-components break React context identity.
  • Two copies of ui-types make structurally identical types nominally distinct — the
    TS2322/TS2345 failure that the @openzeppelin/ui-types override in pnpm-workspace.yaml
    already exists to prevent inside this repo.

Each floor is set to the version UIKit itself requires, which is also the version this change
was verified against. Carets still admit every later 3.x / 4.x release.

Why v2 is not kept via ^2.0.0 || ^3.0.0

Considered and rejected. The same internal coupling means a per-package union range advertises
mixes UIKit itself forbids (ui-components@3 can never sit with ui-utils@2). More decisively,
a range the workspace neither installs nor tests is a claim CI cannot keep — the workspace now
resolves the v3 line, so nothing would signal a v2 regression on any future change.

There is also a mechanical trap: __OZ_PEER_MINIMUMS__ is derived in each tsdown.config.ts by
(range).replace(/^\^/, ''). A union range would bake the literal string "2.0.0 || ^3.0.0" as
the "minimum" and feed it to compareSemver.

Consumers on UIKit v2 must upgrade all four packages together. The baked validatePeerVersions
floor rises with the manifest, so a v2 install now throws at module load with an actionable
message
rather than failing later on a type or prop mismatch.

Regenerated runtime guard

The minimums are generated from the declared peer ranges at build time, so manifest and runtime
guard cannot drift. Confirmed in the built bundles:

$ grep -o '"@openzeppelin/ui-[a-z]*": "[0-9.]*"' packages/adapter-evm/dist/index.mjs | sort -u
"@openzeppelin/ui-components": "3.9.0"
"@openzeppelin/ui-react": "3.3.2"
"@openzeppelin/ui-types": "3.5.2"
"@openzeppelin/ui-utils": "4.0.1"

The workspace dedupe override moved with the floor

pnpm-workspace.yaml pins @openzeppelin/ui-types to force one copy across the workspace.
It moved ^3.5.0^3.5.2 along with the peer floors.

This is not cosmetic housekeeping. The override exists precisely because ui-types resolving
twice makes the structurally identical EcosystemRuntime / CapabilityFactoryMap nominally
distinct, producing TS2322/TS2345 in profiles/shared-state.ts. Now that ui-components,
ui-react and ui-utils each depend on ui-types ^3.5.2 themselves, leaving the override at
^3.5.0 would let pnpm satisfy those independently and reintroduce the second copy. The comment
above the pin was rewritten to record that reason, replacing the stale one about
adapter-runtime-utils still declaring ^3.2.0 — it no longer does.

Verified after install: all seven adapter packages resolve a single ui-types@3.5.2, and the
lockfile contains no other ui-types entry.

One test constant moved

UI_TYPES_FLOOR in packages/adapter-evm/test/sf-5-published-release.test.ts hard-asserts the
ui-types floor on both peerDependencies and devDependencies for adapter-evm and
adapter-evm-core. Raising the floor to ^3.5.2 required moving it. I updated the constant and
recorded the second reason for the move alongside the existing initiative-004 rationale rather
than loosening the assertion. The guard still fires if peer and dev drift apart.

Proof it compiles

This was the part nobody had established — matching export names is not proof that props or
behaviour match across a major. Installed against real UIKit v3 (ui-components@3.9.0,
ui-react@3.3.2, ui-utils@4.0.1, ui-types@3.5.2) and ran the repo's own checks:

check result
pnpm install exit 0
pnpm build (incl. validate:vite-configs, validate:host-peers) exit 0
pnpm typecheck exit 0 — 8/8 packages
pnpm test exit 0 — 3197 passed, 11 skipped, 201 files
pnpm lint exit 0
pnpm format:check exit 0

Per-package test totals:

adapter-runtime-utils   233 passed |  5 skipped (16 files)
adapter-solana           38 passed              ( 6 files)
adapter-midnight        467 passed              (31 files)
adapter-evm-core       1354 passed              (60 files)
adapter-stellar         865 passed |  2 skipped (57 files)
adapter-polkadot         66 passed              ( 6 files)
adapter-evm             160 passed |  4 skipped (22 files)
adapters-vite            14 passed              ( 3 files)

pnpm peers check no longer reports any unmet @openzeppelin/ui-* peer.

Nothing broke between UIKit v2 and v3 on the surface these adapters consume. That is
corroborated independently: the UIKit side diffed every consumed source file and its transitive
import closure from the last 2.x tags to HEAD and found the backing sources byte-identical
except AddressDisplay (purely additive — four new optional props, all new render paths gated
on a resolvedName prop or an AddressNameContext provider) and AppConfigService.ts (whose
only removal is the WalletConnect project-id special case). I confirmed both are inert here:
AccountDisplay.tsx is the single AddressDisplay call site and passes no resolvedName, and
no adapter mounts AddressNameProvider anywhere.

Release

Two changesets. pnpm changeset status --verbose reports:

@openzeppelin/adapters-vite   13.0.0
@openzeppelin/adapter-evm      6.0.0
@openzeppelin/adapter-midnight 6.0.0
@openzeppelin/adapter-polkadot 6.0.0
@openzeppelin/adapter-solana   6.0.0
@openzeppelin/adapter-stellar  6.0.0

Note adapters-vite lands at 13.0.0, not 12.1.0. Widening the Vite peer is genuinely a
minor and its changeset says minor, but adapters-vite peer-depends on the five adapters, so
changesets escalates it to major to ride along with their 6.0.0. That is the tooling behaving
correctly, not the Vite change being breaking.

adapter-evm-core and adapter-runtime-utils are private: true and so are not versioned,
matching existing repo practice. They carry the same ranges because they are bundled into the
published adapters.

Known follow-up, deliberately not in this PR

adapter-evm-core declares react: ^18.0.0 || ^19.0.0 but renders ui-components, which peers
react ^19.0.0 only. An adapter that renders ui-components cannot honestly claim React 18.

I am flagging rather than fixing it: it is pre-existing and unchanged by this PR
(ui-components peered react ^19 in 2.x too), ui-components/ui-react are optional peers
on adapter-evm-core so a headless React 18 consumer is currently legitimate, and tightening to
^19 is a separate breaking change with its own consumer impact. It deserves its own PR and its
own decision.

Why this drifted through two majors unnoticed

The duplicate-release bug above explains how the ranges fell behind. This explains why nothing
ever said so.

validatePeerVersions in ui-utils is floor-only — it throws when
compareSemver(installed, minimum) < 0 and never when installed is above. With minimum
derived from a ^2.0.0 declaration, an installed 4.0.1 passed silently. There was no runtime
signal that the manifest had gone stale, and no install-time one either, because an over-install
is not an unmet peer.

Adding an upper bound to that runtime helper was considered on the UIKit side and rejected,
correctly: it would convert today's benign over-install into a hard startup throw for consumers
on a newer kit, which is a worse failure than the one it prevents, and it would require passing
the range rather than the baked minimum — a joint API change, not a free option. The warning in
oz-ui-dev check-peers (openzeppelin-ui#229) is the right place for this signal, since it
cannot break anyone's CI.

Versions 8.0.0 through 12.0.0 all declared `vite: ^7.0.0`, so no published
version of @openzeppelin/adapters-vite installs against Vite 8 without an
unmet peer.

The package has no runtime coupling to Vite. Its only runtime imports are
node:module, node:fs and node:path; Vite appears solely as
`import type { Plugin, PluginOption, UserConfig }` across config.ts,
integration.ts, registry.ts, resolver.ts and types.ts. All three types still
exist in Vite 8, and the workspace already builds and type-checks against
vite@8.0.5.

Widening a peer range is not breaking, so this releases as a minor.
Every adapter declared UIKit v2 for ui-components, ui-react and ui-utils while
already declaring v3 for ui-types. UIKit's current line is components 3.x,
react 3.x, utils 4.x, types 3.5.x, so no consumer on current UIKit could
install an adapter without unmet peers.

  ui-components  ^2.0.0            -> ^3.9.0
  ui-react       ^2.0.1            -> ^3.3.2
  ui-utils       ^2.0.0            -> ^4.0.1
  ui-types       ^3.2.0 / ^3.5.0   -> ^3.5.2

The floors are exact patch versions rather than ^3.0.0 / ^4.0.0 because the
UIKit packages depend on each other with dependencies, not peers.
ui-components@3.9.0 depends on ui-utils ^4.0.1 and ui-types ^3.5.2, and
ui-react@3.3.2 depends on ui-components ^3.8.3. A major-level floor lets a
consumer satisfy our peer with an older patch while UIKit resolves a second
nested copy for itself. Two copies of ui-utils split its module-level
singletons, appConfigService and Logger.getInstance(), leaving the adapter's
copy uninitialized; two copies of ui-components break React context identity;
two copies of ui-types make structurally identical types nominally distinct,
which is the TS2322/TS2345 failure the ui-types override in
pnpm-workspace.yaml already exists to prevent in-repo.

v2 is not kept via ^2.0.0 || ^3.0.0. The same internal coupling means a
per-package union range advertises mixes UIKit itself forbids, and a range the
workspace neither installs nor tests is a claim CI cannot keep.

The validatePeerVersions minimums are generated from the declared peer ranges
at build time, so the baked floors move with the manifest and a v2 install now
throws at module load instead of failing later on a type or prop mismatch.

UI_TYPES_FLOOR in the SF-5 release-correctness suite moves to ^3.5.2 with the
second reason for the move recorded alongside the initiative 004 rationale.

No source changes were needed. Verified against ui-components@3.9.0,
ui-react@3.3.2, ui-utils@4.0.1 and ui-types@3.5.2: build, typecheck, lint,
format:check and 3197 tests across all eight workspace packages pass.

BREAKING CHANGE: the adapters now require the UIKit v3 line. Consumers on
UIKit v2 must upgrade @openzeppelin/ui-components, ui-react, ui-utils and
ui-types together.
@pasevin
pasevin marked this pull request as ready for review September 9, 2026 12:34
@pasevin
pasevin merged commit cad324d into main Sep 9, 2026
9 checks passed
@pasevin
pasevin deleted the fix/uikit-v3-peers-and-vite8 branch September 9, 2026 12:34
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.

1 participant