MetaMask / MetaMask/metamask-extension

[Epic] `ui/selectors/selectors.js` decomposition (split by domain)

Open
#44,566 1 comment 0 reactions 0 assignees View on GitHub
exalate-duplicate INVALID-ISSUE-TEMPLATE mig-epic-55-child mig-epic-71 team-extension-platform
Dominant language
TypeScript
Stars
13.2k
Forks
5.6k
Avg merge
2d 5h
Merged PRs (30d)
451

Description

**Parent Epic:** [MetaMask-planning#5284 — [EPIC|P2] Typescript](https://github.com/MetaMask/MetaMask-planning/issues/5284) → area sub-epic [#7383 — TypeScript conversion of `ui/contexts`, `ui/helpers`, `ui/selectors`, `ui/ducks`, `ui/hooks`](https://github.com/MetaMask/metamask-extension/issues/7383)
**Labels:** `Epic`, `team-extension-platform`

---

## 🎯 Goal

Split `ui/selectors/selectors.js` (4,042 lines, ~252 selectors) into coherent per-**domain** modules (`ui/selectors/.ts`) — improving modularity, maintainability, and reviewability, and unblocking incremental JS→TS migration (each domain module is independently convertible). Drain the residual `selectors.js` to empty and delete it — no re-export facade (per the no-barrel-files convention); call sites import each selector directly from its domain module.

## Motivation

`selectors.js` is a flat, 252-selector god-file spanning ~13 unrelated domains (preferences, network, permissions, snaps, accounts, tokens, confirmations, …). It is imported by hundreds of call sites via `ui/selectors/index.js` (`export * from './selectors'`), so it is a hub whose churn touches the whole UI, and it cannot be TypeScript-converted coherently in one pass. Splitting by domain shrinks the hub, lets each domain convert independently, and removes duplication with the sibling per-domain modules (`accounts.ts`, `assets.ts`, `transactions.js`, `approvals.ts`, …) that already establish the target pattern. Applies the #41735 decomposition model to the UI selector layer.

## Context (grounding the plan)

- **The base layer is already externalized.** The most-reused hubs (`getCurrentChainId`, `getSelectedInternalAccount`, `getPreferences`, `getRemoteFeatureFlags`, `getInternalAccounts`, `EMPTY_ARRAY`/`EMPTY_OBJECT`) are *imported* from `shared/lib/selectors/*`, `./accounts`, `./shared`. **No new base-selectors module is a prerequisite;** the existing `shared.ts` suffices.
- **Migration seam = direct imports, no barrel** (per the no-barrel-files convention). Do **not** add a `selectors.js` re-export facade — `ui/selectors/index.js` is already a barrel (`export * from './selectors'`) and ~445 sites import through it (37 hit `selectors/selectors` directly). Each domain extraction instead moves its selectors to `ui/selectors/.ts` and repoints every importing site to that module directly, via a `ts-morph` codemod that resolves each named selector to its new home and splits `../selectors` barrel imports into per-module imports. This drains both `selectors.js` **and** the `export * from './selectors'` line out of `index.js` as it goes, adds no new barrel, and keeps each ticket self-contained (move + repoint + delete, no deferred big-bang).
- **One hard ordering constraint: `accounts → permissions → snaps`.** The subject/metadata base (`getPermissionSubjects`, `getSubjectMetadata`, `getTargetSubjectMetadata`) is used by both permissions and snaps, and accounts↔permissions is a bidirectional cycle. Resolution: the connected-account-list selectors (`getUnconnectedAccounts`, `getOrderedConnectedAccountsForActiveTab`, `getUpdatedAndSortedAccounts*`, …) live in `permissions.ts` (one-way permissions→accounts), keeping `accounts.ts` permission-free. Everything else (steps 1–7 below) is parallelizable.

## Scope

Each ticket scaffolds/extends the domain module, moves its selectors + tests, repoints the importing sites to `ui/selectors/` directly (codemod), and deletes the originals — no re-export facade. Ordered lowest-coupling first.

| Order | Ticket | Module | Size | Pri |
|---|---|---|---|---|
| 1 | Preferences + feature flags (~34) | `preferences.ts` | L/S-coupling | P1 |
| 2 | Passkey (4) | `passkey.ts` | S | P2 |
| 3 | Hardware wallet (5) | `hardware-wallet.ts` | S | P2 |
| 4 | MetaMetrics id + data-deletion (~6) | extend `metametrics.js` | S | P2 |
| 5 | Shield + onboarding toggles (9) | extend `shield/`, `onboarding/` | S | P2 |
| 6 | Ephemeral UI/modal state (~15) | `app-ui-state.ts` | M | P2 |
| 7 | Unapproved confirmations / tx detail (~13) | extend `transactions.js` | M | P2 |
| 8 | Network identity/config/chains (~30) | `network.ts` | L | P1 |
| 9 | Legacy tokens / market data / detection (~26, many `@deprecated`) | `tokens.ts` | M/L | P1 |
| 10 | Accounts / keyrings / contacts (~50, hub) | extend `accounts.ts` | L | P1 |
| 11 | Swaps + bridge (6) | `swaps-bridge.ts` | S | P2 |
| 12 | Permissions / subjects / connected sites (~35) | `permissions.ts` | L | P1 |
| 13 | Snaps registry/metadata/interfaces (~30) | `snaps.ts` | L | P2 |
| 14 | Remove `export * from './selectors'` barrel line + delete `selectors.js` | — | M | P1 |

**Order rationale:** steps 1–7 have zero/low intra-file coupling and are parallelizable today. `accounts` (10) is the internal hub and must precede `permissions` (12), which owns the connected-account-list selectors (breaking the cycle) and the subject/metadata base that `snaps` (13) needs. Tokens (9) triages its many `@deprecated` entries (delete vs. fold into `assets.ts`) rather than moving blindly. `selectors.js` and its `index.js` barrel line are removed last, once every domain is out.

## What stays / does not get extracted

- **`shared.ts`** stays as-is (`EMPTY_ARRAY`/`EMPTY_OBJECT`); no expansion.
- **No new `selectors/shared.ts` base module** — the base layer is already external; the only intra-file shared cluster (permission subjects) lives in `permissions.ts`.
- **Residual `selectors.js`** is drained to empty and deleted — there is no facade stage and nothing re-exports through it. The `export * from './selectors'` line is removed from `ui/selectors/index.js` (the existing barrel is not extended, and its selectors line goes away).

## ✅ Success Criteria

- ~13 domain modules exist (new or extended); each `.ts`, independently convertible, with its tests.
- `selectors.js` emptied and **deleted**; the `export * from './selectors'` line removed from `ui/selectors/index.js`; call sites import their selectors directly from the domain modules (no new barrel, existing barrel not extended).
- accounts↔permissions cycle avoided (no `accounts.ts` permission imports); no require cycles.
- Tests pass; no new Sentry signatures.

Contributor guide

Open the contributing guide

Research direction

Start with ui/selectors/selectors.js and ui/selectors/index.js, then compare the existing domain modules such as accounts.ts, assets.ts, transactions.js, and approvals.ts. Map selector ownership and importing sites before using the planned ts-morph codemod. Done means the domain modules and their tests exist, direct imports replace the barrel, the cycle constraints hold, and selectors.js plus its export line are removed.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, typescript
Domain
frontend, tooling
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.