Skip to content

Widget: make optional peer dependencies actually optional #12602

Description

@linear

Problem

packages/swap-widget/package.json marks seven peers optional: true in peerDependenciesMeta. The intent is graceful degradation — a consumer who doesn't install the Solana or Bitcoin packages simply doesn't get those wallets.

That isn't what happens. Every one of the seven is reached by a static value import from the single src/index.ts entrypoint, and tsup emits one flat ESM bundle with all of them marked external. The specifiers sit at the top of dist/index.js, so the consumer's bundler has to resolve them whether or not they ever touch Solana. An EVM-only integrator following the documented install list gets a module resolution failure, not a widget with Solana quietly switched off.

Audit

optional peer static value import reachable from entrypoint via
@reown/appkit WalletProvider.tsx:1, SwapWidget.tsx:3 direct
@reown/appkit-adapter-wagmi config/appkit.ts (WagmiAdapter) SwapWidget → WalletProvider → appkit
@reown/appkit-adapter-bitcoin config/appkit.ts (BitcoinAdapter) same
@reown/appkit-adapter-solana config/appkit.ts (SolanaAdapter), useBalances.ts:1, useSolanaSigning.ts:3 same + TokenSelectModal
@solana/web3.js useBalances.ts:3, utils/addressValidation.ts:2 SwapWidget.tsx:23validateAddress
@solana/wallet-adapter-phantom config/appkit.ts SwapWidget → WalletProvider → appkit
@solana/wallet-adapter-solflare config/appkit.ts same

ESM imports are hoisted and eagerly resolved, so it doesn't help that new PhantomWalletAdapter() only runs inside initializeAppKit() — the import is resolved at module load regardless of whether that function is ever called.

Note that someone already reached for the right technique: useSwapExecution.ts:84 and :156 do await import('@solana/web3.js'). It buys nothing today, because utils/addressValidation.ts:2 value-imports PublicKey from the same package and SwapWidget.tsx:23 pulls that in unconditionally. The lazy path is defeated by a static one two files over.

Options

  1. Separate entrypoint — move the AppKit/Solana surface behind @shapeshiftoss/swap-widget/appkit (or similar) that consumers opt into via exports. Cleanest signal, but changes the package's public shape and the integration docs.
  2. Dynamic imports throughout — convert the adapter construction and the PublicKey/Connection uses to await import(...), guarding the optional-peer paths. Keeps one entrypoint; needs the addressValidation/useBalances call sites to tolerate async.

Either way the install snippets in packages/swap-widget/README.md and packages/public-api/docs/swap-widget-sdk.md need to say which packages are genuinely required for which chain families.

Context

Surfaced during review of #12601, which swapped @solana/wallet-adapter-wallets for the two adapters actually used. That PR doesn't change this situation — one non-functional optional marking became two, contract identical — and its description calls the underlying problem out as a known follow-up. CodeRabbit flagged the two adapters on that PR and was told the same; marking just those two required would have fixed nothing while the other five static imports remained.

Related: @shapeshiftoss/caip statically bundles the generated CoinGecko/CoinCap maps for every chain (~807 KB gzipped, Base alone ~179 KB), which is a similar all-or-nothing weight problem for chain-scoped integrators.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions