Stop plugin app bundles parsing icon barrels - #3918
Merged
ymichael merged 2 commits intoSep 19, 2026
Merged
Conversation
ymichael
force-pushed
the
bb/investigate-uncached-pnpm-start-time-thr_63p2x8iha6
branch
4 times, most recently
from
September 18, 2026 23:46
c167752 to
ac090b4
Compare
Plugin app bundles reached `@hugeicons/core-free-icons` through its ESM barrel, which re-exports 5123 separate modules, and lucide-react through its 1756-module one. esbuild has to resolve, open and parse every one before tree-shaking can drop them. Across the 29 bundled plugin apps that was 31037 modules and 45.3 MB parsed to emit 5.1 MB. Import icons by subpath instead of from the barrel, and move every shared-ui shadcn component off lucide-react onto shared-ui's own `Icon`, which the build shims to the host runtime. `lucide-react` leaves the workspace entirely. Subpath imports need `@hugeicons/core-free-icons` 4.3.x: 4.1.3 publishes no per-icon type declarations, so every deep import would be `any`. The bump is wanted regardless — 4.2.1 stopped pointing the `production` export condition at the unshakeable prebuilt bundle (hugeicons/hugeicons#26), which is the entry a bundler asking for that condition would otherwise select. Splitting the grouped imports exposed five Plugin Guide wireframes drawing a different glyph than the surface they illustrate: the composer mic, settings, edit, side-chat and terminal affordances each used a sibling variant of the icon the app actually renders. They now use the app's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The complete example in the Plugin Guide's sidebar thread-list doc was the last lucide-react import in tracked source. Use the subpath-imported Hugeicons idiom the bundled plugins now use, with the glyphs bb's own ThreadStatusGlyph renders for those two indicators. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ymichael
force-pushed
the
bb/investigate-uncached-pnpm-start-time-thr_63p2x8iha6
branch
from
September 19, 2026 03:53
ac090b4 to
734e8af
Compare
ymichael
deleted the
bb/investigate-uncached-pnpm-start-time-thr_63p2x8iha6
branch
September 19, 2026 04:46
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Human comments
What was wrong
Plugin app bundles reached
@hugeicons/core-free-iconsthrough its ESM barrel, which re-exports 5123 separate modules, and lucide-react through its 1756-module one. esbuild has to resolve, open and parse every one before tree-shaking can drop them.For the tasks plugin that was 6879 of 7263 modules and 7.98 MB of the 10.6 MB esbuild parsed, contributing 11 KB to a 1115 KB bundle. Tree-shaking worked fine; the cost was reaching the point where it could run. It is file-count-bound rather than byte-bound, which is why it showed up as system time.
lucide-reactwas never imported directly byapps/appor any plugin. It entered through twelve shadcn components inpackages/shared-ui, each for one to three glyphs, of which only three were reachable (command,carousel,radio-group).What changed
Icons are imported by subpath, not from the barrel —
import BrainIcon from "@hugeicons/core-free-icons/BrainIcon". 340 imports across 24 files inpackages/shared-ui,packages/plugin-api-map,apps/app,apps/weband four bundled plugins. The alias-only names (FolderIcon→Folder01Icon,GitPullRequestArrow→GitPullRequestIcon) were mapped from the barrel's own export table rather than assumed.@hugeicons/core-free-iconsbumped^4.1.3→^4.3.4. Required: 4.1.3 ships onlydist/types/index.d.ts, so every subpath import would beanyundernoImplicitAny. 4.3.4 ships 6072 per-icon declarations.Every shared-ui shadcn component moves off lucide-react onto shared-ui's own
Icon, which the build shims to thesharedUiIconhost runtime slot, so it costs a plugin bundle nothing.calendar.tsxloses 20 lines where three near-identical chevron branches collapse into one.ArrowLeft,MinusandGripVerticalhad no mapping; the first two were added, the third reuses the existingDragDropVertical.lucide-reactleavespackages/shared-ui,packages/plugin-registryandpnpm-lock.yamlentirely.packages/plugin-registry/r/*.jsonregenerated —lucide-reactleaves every item'sdependencies;@bb/iconjoins theirregistryDependencies. An author runningshadcn add @bb/commandnow gets the glyph from the host runtime instead of bundling an icon library.The Plugin Guide's sidebar thread-list example (
docs/plugin-sidebar-thread-list.md) was the lastlucide-reactimport in tracked source. It now uses the subpath-imported Hugeicons idiom the bundled plugins use, with the glyphsThreadStatusGlyphactually renders for those two indicators (Loading03Icon,HelpCircleIcon).apps/apptheme.css/disclosure.tsx—.lucide { stroke-width: var(--icon-stroke-width) }renamed.bb-icon-stroke, matching thebb-prefix the other app-owned classes in that file use. Its one caller also carried a deadstrokeWidth="0.75"(a CSS rule beats a presentation attribute regardless of@layer, so the class always won).Five Plugin Guide wireframes corrected. Splitting the grouped imports made visible that
packages/plugin-api-map/src/wireframes.tsxdrew several affordances with a different glyph than the surface it illustrates. Each now uses the icon the app actually renders:Mic01IconMic02Icon(Icon name="Mic")Settings02IconSettings01Icon(Settings)PencilEdit01IconEdit02Icon(Edit)MessageAdd01IconMessageAdd02Icon(SideChat)TerminalIconComputerTerminal01Icon(Terminal)All five predate this PR. A sweep of the file's 26 icons now finds one left outside the app's palette —
SparklesIcon, for which the app has no equivalent, so it stays.packages/plugin-buildis untouched. BB's resolver behaviour is unchanged —git diff main -- packages/plugin-buildis empty.Numbers
Modules esbuild parses per plugin app bundle, from its metafile. Deterministic, not timing:
−94% modules, −76% bytes parsed. Combined output across all 29 bundles moves 5124565 → 5121854 bytes (−0.05%) — the same artifacts, without reading 29k files to produce them.
prepare:bundledwall time, before/after interleaved per plugin, min of 5 runs. This machine is shared, so min is the least contaminated statistic; the last two rows are controls that touch neither barrel:How you verified
pnpm exec turbo run typecheck lint test --filter='...[origin/main]'— 163 of 169 tasks pass; see the exception below.origin/main(13 commits, including plugin manifest edits from Improve CLI recovery, unify plugin commands, and retain terminal output #3913 that touch the samepackage.jsonfiles): rebase applied cleanly,pnpm install --frozen-lockfilereports the lockfile already consistent, andturbo run typecheck lint --filter='...[origin/main]'is green across all 33 in-scope packages (38/38 tasks).wireframes.tsxto its actual module (aliases likeFolderIcon→Folder01Icon.jsmake names misleading) and compared against the app's palette. 25 of 26 now match;SparklesIconis the deliberate exception.Copy01Icon,Mic01Icon,Mic02Icon,SquareUnlock02Icon,UserAdd01Icon,UserIcon,UserSwitchIcon). A first pass flagged all 181 because 4.3.4 drops the/*#__PURE__*/annotation; the comparison ignores it.app.cssbyte-identical;app.jsdiffers by 29 bytes of minifier naming; both bundles contain the same 43 SVG path strings.theme.csssets--icon-stroke-width: 1.75and the old.luciderule applied it, so lucide glyphs never rendered at their native stroke-2; the real change is 1.75 → hugeicons' baked 1.5.thread--timeline--rows--reasoning--completedstory.@layer baseblock andgenerate-plugin-theme.mjsonly extracts@themeblocks; regenerating confirms no change.Not verified:
@bb/desktop'spreload-build.test.tstimes out here — it spawns a real Electron window and reports no output (391 of 393 desktop tests pass). It has no dependency on anything in this diff and only re-runs because the lockfile edit invalidates turbo's global hash, but I could not get it green in this environment.Behaviour changes worth a second opinion
CommandInput(machine / project / parent-thread / environment pickers, tasks plugin),Carousel(PluginMarketplaceListing) andRadioGroup(keep-awake), from the lucide removal.lucide-reactdirectly lose the host's 1.75 stroke normalisation and fall back to lucide's own stroke, since.lucideno longer exists.Why not the
productionexport conditionAn earlier revision of this PR set
conditions: ["production"]on the plugin app build, which resolves hugeicons to its prebuilt single-file bundle. Two findings killed it.It is a no-op on any current hugeicons. 4.2.1 repointed that condition at the same tree-shakeable entry as
default, so it only does anything on ≤4.2.0 — the version we were pinned to. Of 185 marketplace plugin apps, 103 depend on hugeicons and 97 of those declare^4.1.3, which installs 4.3.4 today: no benefit for any of them.More to the point, upstream removed it deliberately, in hugeicons/hugeicons#26 — "production export condition breaks tree-shaking on rspack/Vite". Their guidance is to keep the minified single-file build "for CDN/script use, just not as the bundler entry". Subpath imports are what the package is designed for, and they measure better anyway: 10.9 MB parsed versus 36.7 MB for the condition.
Checked while evaluating that revision: of the 119 distinct runtime dependencies across all 185 marketplace plugin apps, only two declare a
productioncondition — hugeicons and@excalidraw/excalidraw, whose root entry listsdefaultandproductionas the same file.