Skip to content

refactor(web): route all icon imports through one barrel - #7963

Open
saphid wants to merge 7 commits into
pingdotgg:mainfrom
saphid:agent/web-icon-barrel-extracted
Open

refactor(web): route all icon imports through one barrel#7963
saphid wants to merge 7 commits into
pingdotgg:mainfrom
saphid:agent/web-icon-barrel-extracted

Conversation

@saphid

@saphid saphid commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

What Changed

  • One outcome: every web interface-glyph import now flows through one generated ~/icons barrel, and a lint rule blocks direct lucide-react imports.
  • Changes:
    • apps/web/scripts/generate-icon-barrel.ts — scans apps/web/src and apps/web/test for icon imports, validates runtime names against lucide-react, and writes src/icons/index.ts as a pure re-export; --check verifies freshness without writing.
    • apps/web/src/icons/icon-import-parser.ts — shared import parser + name validation, with icon-imports.test.ts / index.test.ts coverage.
    • ~140 component, route, and test files switch their import source from lucide-react to ~/icons. No call-site logic changes.
    • vite.config.tsno-restricted-imports now forbids root and subpath lucide-react imports outside the barrel, pointing at the generator.
    • docs/internals/icons.md — maintainer doc for the seam and the two scripts.
    • Refresh commit 3b3af3cb4 — the parser now strips block and line comments inside import braces (review finding; see Verification).
  • Preserves: the exact Lucide component objects every call site receives. The barrel is a pure re-export.
  • Intentionally does not change: icon choices, rendered component behavior, props, layout, animation, interactions, React Native mobile, native SwiftUI mobile, desktop main/preload, providers, contracts, server behavior, and local/remote/relay/tunnel connection behavior.

Why

Reproduction:

  1. Pick almost any component under apps/web/src.
  2. Its glyphs import directly: import { XIcon } from "lucide-react" — across ~140 files.
  3. Try to audit or replace the interface icon set: a repository-wide call-site sweep with nothing enforcing the result.

Actual: no owned icon module, no enforcement; new direct imports could land at any time.

Expected: one module owns interface glyphs, a lint rule keeps every import on it, and regeneration is a single command.

Cause: the web client never grew an icon module or a lint rule for Lucide imports.

Why this owner and approach: a generated pure re-export barrel is the smallest seam that changes nothing at runtime while making ~/icons the single plug-in point for any future icon-pack decision; scanning real usage keeps the barrel exactly as large as the app; the lint rule makes the seam the only path, so it cannot rot silently.

Refs: no upstream issue — maintenance refactor.

UI Changes

No user-visible change, verified by identical captures. The Settings screen (/settings/general — the most icon-dense surface: sidebar nav with eight section icons, search, and panel row icons) was captured at the merge-base build and at this head:

  • before build: d22709f75 (merge-base of this branch and origin/main), built the same way as the head build
  • after build: 3b3af3cb4
  • both dev servers ran sequentially against the same isolated, empty home directory, so every rendered byte of text (including the displayed base-dir path) is identical; no personal threads, projects, or hosts appear — sanitized by construction
  • headless Chrome 151, viewport 1440×900 @2x, appearance driven by emulated prefers-color-scheme

Result: SHA-256 identical for all four frame pairs; ImageMagick compare -metric AE reports 0 of 5,184,000 pixels differ — full window and sidebar crop, in both dark and light. Both appearances are captured even though the claim is appearance-independent (an import reroute cannot depend on theme); dark is shown first and all light media is collapsed below.

Fidelity note: an earlier capture round differed by 13 single pixels on text-antialiasing edges (0.00025%); two captures of the same merge-base build differ from each other by the same 13 pixels, so that is capture-time rasterization noise, not the code change. The published frames below are from the byte-identical round. The comparison GIFs look static because the frames are identical — that is the evidence.

Dark mode

Before

Settings screen sidebar, dark mode, merge-base build

After

Settings screen sidebar, dark mode, PR head

Before/after comparison GIF

Alternating merge-base and head frames of the Settings sidebar, dark mode — visually static because the frames are pixel-identical

Full-window context, dark mode

Before (merge-base d22709f75):

Full Settings window, dark mode, merge-base build

After (head 3b3af3cb4):

Full Settings window, dark mode, PR head
Light mode evidence

Before

Settings screen sidebar, light mode, merge-base build

After

Settings screen sidebar, light mode, PR head

Before/after comparison GIF

Alternating merge-base and head frames of the Settings sidebar, light mode — visually static because the frames are pixel-identical
Full-window context, light mode

Before (merge-base d22709f75):

Full Settings window, light mode, merge-base build

After (head 3b3af3cb4):

Full Settings window, light mode, PR head

No action sequence is included: the PR changes no interaction, so there is no action to record.

Verification

All results are real, at final head 3b3af3cb4:

  • vp i — exit 0 (fresh worktree install).
  • CI=true vp test run apps/web/src/icons/icon-imports.test.ts apps/web/src/icons/index.test.ts — exit 0; 2 files, 5 tests passed (includes a new regression test for commented imports).
  • CI=true vp test run apps/web/src/components/chat/ComposerStashMenu.test.tsx — exit 0; 1 file, 4 tests passed (smallest existing slice importing the barrel; also covers the file touched by the feat(web): attach PDFs, ZIPs, and other files to a turn #8236 rebase conflict).
  • CI=true vp run --filter @t3tools/web icons:check — exit 0; verified 201 generated exports.
  • CI=true vp run --filter @t3tools/web typecheck — exit 0.
  • CI=true vp lint apps/web/src/icons/icon-import-parser.ts apps/web/src/icons/icon-imports.test.ts — exit 0; no errors.
  • CI=true vp fmt --check apps/web/src/icons/icon-import-parser.ts apps/web/src/icons/icon-imports.test.ts — exit 0.
  • Integrated (web): worktree-isolated dev server with pairing auth, /settings/general captured at merge-base and head in dark and light — SHA-256 equal, 0 pixels differ (media above). macOS 26, headless Chrome 151, dev variant, empty isolated state.
  • Review threads: 2 unresolved at refresh start. The comment-handling finding is fixed at 3b3af3cb4 with a regression test; the declaration-level type-import finding was already handled at the current head — evidence replies posted on both threads.

Risks and limitations

  • Base freshness at publication: fetched origin/main tip d22709f75 equals the merge-base; git merge-base --is-ancestor origin/main HEAD exits 0 — the branch is 0 commits behind base and needed no rebase in this refresh.
  • The generator supports the repository's named-import form. Default, namespace, dynamic, and direct subpath call-site imports are forbidden by lint rather than converted.
  • icons:check is not a separate CI job. A missing barrel export already fails the required web typecheck; the explicit check catches dead exports and ordering drift during maintenance.
  • Media captures use an empty isolated environment (sanitized by construction). No multi-window or remote-connection surfaces were re-captured — no behavior changed for them.
  • No human maintainer review has happened on this head. Green automation and bot summaries are not human approval.
  • A fresh cross-provider review of the refresh commit was not launched: this refresh duty explicitly requires a single worker with no re-delegation. The two bot findings were verified against source by hand instead.

Checklist

  • This PR is small and focused (one mechanical outcome; 144 files move their import source, no logic edits)
  • I explained what changed and why
  • I included before/after screenshots for any UI changes (identical captures proving no visual change)
  • I included a video for animation/interaction changes (not applicable — no animation or interaction change)
  • Dark mode is shown first; every light image and GIF sits inside one collapsed details block; primary crops stay readable at phone width; matching full-window captures are in collapsed context sections
  • Every before/after pair has an alternating comparison GIF (visually static — the frames are pixel-identical)
  • All media was captured at the final head after the last change, and every embedded URL was verified to return HTTP 200
  • The branch contains no unrelated, generated, personal, or private files; media lives on a media branch, not in the diff
  • Every stated validation result above is a real, current result

Original implementation and rebase by GPT-5.6 Sol using the Codex harness in T3 Code. This refresh — the review-finding fix, the pixel-identity media, and this body — by GLM 5.3 using the Claude Code harness in T3 Code.

Note

Route all apps/web lucide-react imports through ~/icons barrel

  • Adds generate-icon-barrel.ts to scan source files and generate src/icons/index.ts with the used Lucide icons.
  • Updates apps/web components to import icons from ~/icons instead of lucide-react.
  • Enforces the pattern with a no-restricted-imports ESLint rule in vite.config.ts.
  • Risk: The ESLint rule in vite.config.ts fails on direct lucide-react imports. Developers must run icons:generate to update the barrel before using new icons.

Macroscope summarized 6b9f440.


Note

Low Risk
Mechanical import reroute with identity-tested re-exports; main risk is developer friction if someone bypasses ~/icons or forgets to regenerate the barrel.

Overview
All web UI glyphs now import from ~/icons instead of lucide-react. The new generated barrel in src/icons/index.ts is a pure Lucide re-export so runtime components stay the same; ~140 call sites only change their import path.

Tooling and enforcement: scripts/generate-icon-barrel.ts scans src and test, validates names against Lucide, and writes the barrel (icons:generate / icons:check). icon-import-parser.ts powers that scan with tests for comments, JSX, and literals. Root ESLint no-restricted-imports blocks direct lucide-react imports outside the barrel. Maintainer docs describe the workflow.

No product behavior change — this is a single seam for future icon-pack swaps and consistent enforcement when adding icons.

Reviewed by Cursor Bugbot for commit 6b9f440. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: efbe9b2c-00e0-4e47-84e5-3ffb31fb6aa9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Aug 23, 2026
Comment thread apps/web/scripts/generate-icon-barrel.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

UI consistency: one web file still bypasses the new ~/icons seam

apps/web/src/components/chat/ChatComposer.tsx (import block ending at line 237) still imports directly from lucide-react:

import {
  BotIcon,
  CircleAlertIcon,
  PencilRulerIcon,
  type LucideIcon,
  LockIcon,
  LockOpenIcon,
  PenLineIcon,
  SparklesIcon,
  XIcon,
} from "lucide-react";

Two consequences that come from lines changed in this PR:

  1. The new eslint/no-restricted-imports entry added in vite.config.ts is "error" and applies repo-wide, so lint now fails on this file. It carries no eslint-disable, and the PR's verification only ran vp lint apps/web/src/icons, which would not surface it.
  2. The composer is the most icon-dense surface in the app, so leaving it outside the barrel contradicts the invariant this PR and docs/internals/icons.md state ("Every interface glyph in apps/web flows through one module") and would be missed by a future icon-pack swap — the exact seam this change exists to create.

Smallest fix: change that one specifier to ~/icons. Every name in the block, including type LucideIcon, is already exported by apps/web/src/icons/index.ts, so the barrel does not need regenerating:

} from "~/icons";

(Posted as a conversation comment rather than inline because the file is not part of this diff.)

Posted via Macroscope — UI Consistency

@macroscopeapp

macroscopeapp Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — The UI changes are intended as a pure Lucide re-export refactor, but the PR adds a substantial custom TSX scanner that controls the generated production icon barrel. An unresolved concrete concern indicates JSX attribute expressions can cause later icons to be omitted during regeneration, so the tooling and its bundle impact require human review.

You can add or adjust custom eligibility rules. Learn more.

@saphid

saphid commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

Good catch — fixed in c67ca0c. The codemod's grep had skipped ChatComposer.tsx because the file contains literal NUL bytes in a template string (grep binary detection); the sweep now reads file bytes instead, and a re-scan confirms zero direct lucide-react imports remain outside the barrel. Macroscope's UI Consistency check passes on the latest commit.

@saphid

saphid commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Factual check on the Macroscope UI-consistency comment against current head c67ca0c5: apps/web/src/components/chat/ChatComposer.tsx imports the icon block from ~/icons at line 237. The file contains a NUL byte, which explains why text-oriented scans can misclassify it as binary. No code change is needed for that finding.

The separate unresolved generator finding is still valid: declaration-level import type { LucideIcon } from "~/icons" is matched as a runtime export by IMPORT_PATTERN. That needs a code fix and focused test before the PR is ready.

@saphid
saphid force-pushed the agent/web-icon-barrel-extracted branch 2 times, most recently from 4e958a7 to 7e8c6c1 Compare August 27, 2026 02:32
Comment thread apps/web/src/icons/icon-import-parser.ts
@saphid
saphid force-pushed the agent/web-icon-barrel-extracted branch 3 times, most recently from 53f060e to c0fb01a Compare August 29, 2026 05:16
@saphid
saphid force-pushed the agent/web-icon-barrel-extracted branch from c0fb01a to d0a4c9c Compare August 29, 2026 05:21
collectRuntimeIconNames treated block and line comments inside import
braces as part of member names, so icons:generate and icons:check threw
on valid TypeScript like `import { /* primary action */ XIcon } from
"~/icons"`. Strip comments from the brace contents before splitting
members, with regression coverage for inline, multiline, and trailing
comment forms.

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread apps/web/src/icons/icon-import-parser.ts Outdated
Comment thread apps/web/src/icons/icon-import-parser.ts Outdated
Comment thread apps/web/src/icons/icon-import-parser.ts
Comment thread apps/web/src/icons/icon-import-parser.ts
Comment thread apps/web/src/icons/icon-import-parser.ts Outdated
Comment thread apps/web/src/icons/icon-import-parser.ts Outdated

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6b9f440. Configure here.

characters[index] = " ";
}
continue;
}

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.

JSX tag state ignores attribute expressions

Medium Severity

jsx-tag never enters attribute {...} expressions and treats every > as the tag closer. Common TSX such as onClick={() => ...} or count={n > 0} drops the scanner into jsx-text, which blanks later ~/icons imports so the generated barrel can omit them.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6b9f440. Configure here.

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

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant