Skip to content

chore(deps): lucide-react 1.31.0 -> 1.43.0, with the one retired spelling repaired - #8941

Draft
os-zhuang wants to merge 4 commits into
mainfrom
claude/pr-7058-lucide-react-1.41.0
Draft

chore(deps): lucide-react 1.31.0 -> 1.43.0, with the one retired spelling repaired#8941
os-zhuang wants to merge 4 commits into
mainfrom
claude/pr-7058-lucide-react-1.41.0

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Supersedes the stale Dependabot branch objectui#7058 (dependabot/npm_and_yarn/lucide-react-1.35.0). That branch is not pushed to here — Dependabot force-pushes over it on every rebase — so this is a fresh branch cut from current main. Once this lands, objectui#7058 can be closed as superseded. It is deliberately not named with a closing keyword above; closing it is a maintainer action, not this PR's.

1. Which of the seven red checks were staleness, and which were real

objectui#7058's base is 4e3a4f07e — weeks behind main. I brought its head up to date in a throwaway worktree, re-locked from current main, and re-measured. Four of the seven were artefacts of the stale base; three were real.

Check on objectui#7058 Verdict Evidence
Type Check REAL The job failed at pnpm check:icon-record-names, not at tsc. tsc never ran.
Test (shard 1/4) REAL packages/cli/src/__tests__/app-generator.test.ts — the generator's literal lucide-react range stayed at ^1.31.0 while the 22 sibling manifests moved.
Test (shard 3/4) MIXED check-lucide-icon-record-names.test.ts REAL (same four sites). ci-cd-pipeline-doc.test.ts STALE — the lockfile carried two @objectstack/spec versions.
Test (shard 4/4) MIXED doc-version-claims.test.ts REAL (skills/objectui/guides/plugin-development.md still taught ^1.31.0). check-lockfile-integrity.test.ts STALE — its fixture forks a zod@4.4.3 key the stale lockfile had replaced with 4.5.4.
Lockfile Integrity Check STALE VERDICT 12 finding(s): 7 backward move(s), 5 duplication(s) — Dependabot re-locked on an old base and dragged @objectstack/client, core, formula, lint, sdui-parser, types from 17.4.0 back to 17.3.0, plus a zod 4.4.3/4.5.4 split.
Bundle Analysis STALE vendor-objectstack 2874.0 KB / 1224.6 KB ceiling (OVER by 1649.3 KB) — that is the duplicated @objectstack tree above, which the lockfile gate itself names as the cause: "a bundle-size red downstream of this is caused by THIS, not by the dependency being bumped."
dependabot STALE Dependabot's own branch check.

On this branch, re-locked from current main, node scripts/check-lockfile-integrity.mjs reports VERDICT clean — no @objectstack/* identity moved backward and no package gained a copy.

2. Root cause of what survives — measured against the installed artifact

The hypothesis in the dispatch was a broad source-compatibility break. The measurement narrows it to a single retired name, and falsifies the type-checking half entirely.

I installed 1.31.0, 1.41.0 and 1.43.0 side by side and diffed three surfaces:

Surface 1.31.0 1.43.0 Removed
runtime exports of lucide-react 6068 6224 0
public names in lucide-react.d.ts 6072 6230 0
iconNames from lucide-react/dynamic.mjs 2025 2077 0
keys of the runtime icons record 1767 1818 1 — Trash2

Then I enumerated what this repository actually imports — 4700 source files, 333 distinct value names, one type name (LucideIcon), six namespace or default imports — and checked every one against both artefacts. Zero fail to resolve, in either version. Controls fired on the same run in both worlds: Trash resolves in both, a fabricated name resolves in neither.

So nothing breaks at an import and nothing breaks in the compiler. Trash2 is still exported by 1.43.0; it is simply no longer a key of the runtime icons record, and Trash2 === Trash there (in 1.31.0 they were distinct objects). That is exactly the class scripts/check-lucide-icon-record-names.mjs exists to catch: a retired spelling still imports, still type-checks, still renders as a COMPONENT, and resolves to nothing as a STRING.

Four authored sites resolve trash-2 through renderers/action/resolve-icon.ts, which reads record membership:

  • examples/schema-catalog/src/schemas/actions/action-button-variants.json
  • examples/schema-catalog/src/schemas/actions/confirmation-dialog.json
  • examples/schema-catalog/src/schemas/components-layout-card/profile-detail-card.json
  • packages/plugin-detail/src/DetailView.tsx — the record delete action

All four now write trash. The glyph does not change: 1.43.0's trash icon node is byte-identical to 1.31.0's trash-2 icon node (control: pencil is byte-identical across both versions, so the comparison is not returning equal for everything). Upstream replaced the old bin artwork under the trash name and kept trash-2 as a deprecated alias.

3. Why 1.43.0 and not the 1.41.0 the Dependabot PR advertises

The workspace declares a caret range, so a re-lock from current main resolves the newest 1.x — that is 1.43.0 today, under ^1.41.0 just as much as under ^1.43.0. Holding exactly 1.41.0 would need an exact pin or an override, which is not this repo's convention and not mine to invent. Measured, the extra two minors cost nothing in removals: 1.41.0 to 1.43.0 removes zero exports, zero record keys and zero dynamic names.

It does bring one behavioural change that 1.41.0 does not have, and it is worth a maintainer's eye:

1.43.0 reshaped createLucideIcon to take an icon-data object carrying aliases, and it now emits one CSS class per retired alias. A spinner renders class="lucide lucide-loader-circle lucide-loader-2 ...". Measured: 248 of 1818 icon modules carry an alias; 1.31.0 icon modules have no aliases field at all. This is additive, so a .lucide-loader-circle selector still matches — but a selector asserting the exact, complete class string does not, and neither does an assertion that a given lucide- class is ABSENT.

I censused the whole tree for both shapes, using the alias index built from the installed artefact as the oracle: 28 distinct lucide- class names are referenced anywhere in the repository (all file types, snapshots included) and exactly two are aliases in 1.43.0lucide-home (owned by house) and lucide-loader-2 (owned by loader-circle). Controls: injecting the known alias trash-2 into the census is found; check is correctly absent. Both affected pins are repaired here:

  • examples/schema-catalog/test/svg-host-dom-leak-5632.test.tsx asserts the exact attribute set. The added class is named. The assertion stays exact on purpose — that list is what catches an authored prop reaching the DOM, which is the whole point of objectui#5632.
  • packages/components/src/renderers/form/__tests__/button-shared-icon-resolver.test.tsx proved home resolved through the rename to House by asserting svg.lucide-home is null. house now emits lucide-home itself, so that class no longer says which glyph was drawn. Replaced with a form that still fires for the same defect: exactly one svg, and the alias class on that same element. Had the rename been dropped, Home is not a record key and the button would render no glyph at all.

4. Something the bump gives back

main installs two physical copies of the icon library: 1.31.0 for this workspace and 1.35.0 pulled by fumadocs-ui. On this branch both collapse onto a single 1.43.0. Lockfile resolutions go 1822 to 1821.

5. What was measured here

  • pnpm check:icon-record-namesOK ... 184 authored/declared names reaching 1 record-reading resolver are live icons keys (record 1818 keys).
  • Reverse verification, direction predicted before running: restoring trash-2 at DetailView.tsx:929 turns that gate red naming exactly that one site and nothing else; restoring the file returns it to green. Restoration proved by blob hash equality with HEAD and an empty git diff HEAD, not by an exit code.
  • pnpm type-check across the workspace — turbo 81 successful, 81 total over 45 packages, zero error TS.
  • pnpm exec vitest run packages/components/ examples/schema-catalog/ packages/plugin-view/ packages/plugin-list/Test Files 395 passed (395), Tests 5688 passed (5688).
  • pnpm exec vitest run packages/plugin-detail/ packages/types/ examples/schema-catalog/ — 350 files, 6769 tests, green after the pin repair.
  • pnpm exec vitest run packages/cli/src/__tests__/app-generator.test.ts — 43 passed.
  • pnpm exec vitest run scripts/__tests__/doc-version-claims.test.ts — 29 passed.
  • pnpm exec vitest run scripts/__tests__/check-lucide-icon-record-names.test.ts scripts/__tests__/ci-cd-pipeline-doc.test.ts scripts/__tests__/check-lockfile-integrity.test.ts — green.
  • node scripts/check-lockfile-integrity.mjsVERDICT clean.
  • node scripts/check-changeset-presence.mjs and check-changeset-no-major.mjs — both green.

Declared narrowing: the full four-shard pnpm test and the repo-wide pnpm lint are left to CI. eslint over the changed source files reports one pre-existing error at action-bar.tsx:267 in code this PR does not touch (its only change to that file is a docblock comment); that attribution is stated, not yet controlled against main. The Bundle Analysis measurement was still building locally when this was opened and is reported as NOT MEASURED rather than assumed — the stale-base cause of its red is independently established above.

6. Governed surface — this PR stays a draft

node scripts/check-governed-queue-guard.mjs --test reports GOVERNED — 1 of 37 path(s): skills/objectui/guides/plugin-development.md, the published skills catalog. One governed path governs the whole PR. It is not to be flipped ready, enqueued, or armed with auto-merge; a human merge is the review record.

The changed line there is the plugin skeleton's lucide-react range, which doc-version-claims.test.ts anchors to the in-repo range. Its KNOWN_CLAIMS entry is keyed by the literal text, so the inventory key moved with it.

Session that produced this: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH

🤖 Generated with Claude Code

https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH


Generated by Claude Code

os-support-ai and others added 3 commits September 10, 2026 04:38
…tired spelling

Supersedes the stale Dependabot branch for this bump (objectui#7058), whose
base was weeks behind `main`.

Measured against the installed artifact, not the changelog: across the whole
1.31.0 -> 1.43.0 jump lucide removes ZERO public names from `lucide-react`
(6072 -> 6230 d.ts names, none dropped; 6068 -> 6224 runtime exports, none
dropped) and ZERO names from `lucide-react/dynamic.mjs`. Exactly one key
leaves the runtime `icons` record: `Trash2`, which upstream retired in favour
of `trash` (lucide-icons/lucide#3141). It survives as a DEPRECATED EXPORT of
the same object, so nothing breaks at a named import or in the type checker --
only STRING lookups through `resolveIcon`, which read record membership.

Repaired, all four sites the gate names:
  - examples/schema-catalog .../action-button-variants.json
  - examples/schema-catalog .../confirmation-dialog.json
  - examples/schema-catalog .../profile-detail-card.json
  - packages/plugin-detail/src/DetailView.tsx

`trash` is the visually lossless replacement: lucide 1.43.0's `trash` icon
node is byte-identical to 1.31.0's `trash-2` icon node (control: `pencil`
identical across both versions).

Also re-anchored the two ranges that quote the workspace:
  - packages/cli/src/utils/app-generator.ts (app-generator.test.ts anchor)
  - skills/objectui/guides/plugin-development.md (doc-version-claims anchor)

and the four doc/comment examples that taught the retired spelling.

The lockfile is regenerated from current `main`, so the @objectstack 17.4.0 ->
17.3.0 downgrade, the zod 4.4.3/4.5.4 split and the resulting duplicated
vendor chunk that reddened objectui#7058 are all absent here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
…alias classes retired

lucide-react 1.43.0 reshaped `createLucideIcon` to take an icon-data object
carrying `aliases`, and it now emits one CSS class per retired alias. Measured
on the installed artifact: 248 of 1818 icon modules carry an alias; 1.31.0 icon
modules have no `aliases` field at all.

Censused the whole tree for the two shapes this can break, using the alias
index as the oracle: 28 distinct `lucide-*` class names are referenced anywhere
in the repository (all file types, snapshots included) and exactly two of them
are aliases in 1.43.0 -- `lucide-home` (owned by `house`) and `lucide-loader-2`
(owned by `loader-circle`). Both are repaired here; the other 26 still name a
canonical icon and stay discriminating. Control: injecting the known alias
`trash-2` into the census is found, and `check` is correctly absent.

  - examples/schema-catalog svg-host-dom-leak-5632: asserts the EXACT attribute
    set, so the added class has to be named. Kept exact on purpose -- that list
    is what catches an authored prop reaching the DOM.
  - components button-shared-icon-resolver: its negative half read
    `querySelector('svg.lucide-home')` is null to prove `home` resolved through
    the rename to House. `house` now emits `lucide-home` itself, so that class
    no longer says which glyph was drawn. Replaced with a form that still fires
    for the same defect: one svg, and the alias class on that same element.

Also adds the changeset the four touched released packages owe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
Conflicts, both taken deliberately:
  - apps/site/package.json — kept our `lucide-react: ^1.43.0` AND main's
    `next: 16.3.3`. Neither side's other change is dropped.
  - pnpm-lock.yaml — took main's file and regenerated it with `pnpm install`
    rather than hand-merging YAML. Verified after: exactly one
    `lucide-react@1.43.0`, and every `@objectstack/*` package resolves to a
    single 17.4.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3504.9 KB 3512.7 KB
Main entry chunk (gzip) 144.1 KB 350 KB
Entry file index-B6n3sok8.js
Status FAIL

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.

Which half objected:

Eager-closure half Verdict
Aggregate closure ceiling ✅ pass
Per-chunk ceilings ❌ over its ceiling
Ceiling sensitivity (headroom) ✅ pass
Ceiling freshness (checkout vs. base branch) ✅ pass

📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.20KB 114.67KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 28.04KB 9.46KB
data-objectstack (index.js) 201.97KB 56.22KB
fields (index.js) 246.97KB 62.30KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.54KB 46.34KB
plugin-dashboard (index.js) 132.41KB 34.84KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 253.19KB 65.62KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.96KB 40.93KB
plugin-grid (index.js) 210.86KB 57.28KB
plugin-kanban (index.js) 57.58KB 16.47KB
plugin-list (index.js) 112.54KB 27.65KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 83.34KB 27.61KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 2.71KB 1.35KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…de 1.43.0

Measured on this branch's console build: `ui-components` is 410,904 bytes
gzipped against a 399,000 ceiling -- over by 11,904.

Ruled out as a tree-shaking regression BEFORE the number moved, because "the
bundler stopped shaking" and "the dependency got bigger" want opposite fixes
and only one of them is a ceiling:

  - ALL 1,818 keys of lucide 1.43.0's runtime `icons` record appear verbatim in
    the built chunk. Of 1.31.0's 1,767 keys, 1,766 appear in the same chunk --
    the one absent is `Trash2`, the key lucide retired, which is what shows the
    probe discriminates rather than matching everything. A fabricated key is
    not found.
  - Icons therefore do not tree-shake here and did not before. That is
    deliberate: `resolve-icon.ts` imports the whole record because string
    lookups read record MEMBERSHIP (the objectui#5935 seam). Moving it to
    `lucide-react/dynamic.mjs` to shed bytes would resolve the very names the
    record drops -- `trash-2`, `edit`, `smile` -- which the icon gate's own
    header calls worse than having no gate.
  - The library is bigger two ways. Same-artwork control: `pencil.mjs`, whose
    icon node is byte-identical across versions, goes 455 -> 522 bytes because
    1.43.0 reshaped every icon module into an `__iconData` object carrying
    `name`, `size` and `aliases`. And there are more icons: the icon directory,
    licence headers stripped and gzipped, goes 166,705 -> 176,968 bytes
    (+10,263, +6.2%) -- the same order as the chunk delta.

Ceiling and baseline move together, in this one commit. New headroom is 9,096
bytes = 0.10x REGRESSION_THIS_GATE_MUST_CATCH_BYTES, the proportion
objectui#7399 re-pinned this key to -- chosen over "just enough to pass",
because a line left with almost no headroom is the defect objectui#8816
records. REGRESSION_THIS_GATE_MUST_CATCH_BYTES did not move. Nothing else
moved: the aggregate was re-read on the same build and passes on its own
(3,589,068 of 3,597,000, headroom 7,932), so it is left alone.

The raised gate still fails: padding `ui-components` by 15,000 bytes in a COPY
of the report gives `OVER by 5.8 KB` and exit 1, while the same invocation on
an unpadded copy exits 0. The real report was never mutated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH

Copy link
Copy Markdown
Contributor Author

Bundle Analysis — measured, root-caused, and the ceiling raised deliberately

Re-measured on my own merge result (e85ffc530), not read off another seat's report. Only one line objects:

✅ Console eager closure is 3504.9 KB gzipped across 50 of 518 chunks (budget: 3512.7 KB, headroom: 7.7 KB).
❌ 1 eager chunk is over its per-chunk budget:
  ✅ vendor-objectstack      1207.3 KB / 1224.6 KB ceiling (headroom 17.3 KB)
  ✅ i18n-locales             443.9 KB / 444.3 KB ceiling (headroom 0.4 KB)
  ❌ ui-components            401.3 KB / 389.6 KB ceiling (OVER by 11.6 KB)
  ✅ framework                 71.8 KB / 97.7 KB ceiling (headroom 25.9 KB)

Exactly ui-components, 410,904 bytes against a 399,000 ceiling — over by 11,904. Worth noting on the way past: vendor-objectstack is 1207.3 KB here versus 2874.0 KB on objectui#7058's head, which is the duplicated @objectstack tree from its stale lockfile disappearing. That half of the red was staleness.

First: is this tree-shaking, or is the library bigger?

Asked before touching a ceiling, because the two want opposite fixes.

It is not a tree-shaking regression. All 1,818 keys of lucide 1.43.0's runtime icons record appear verbatim in the built chunk. Not most — all. Of 1.31.0's 1,767 keys, 1,766 appear in that same chunk; the single absent one is Trash2, the key lucide retired. That asymmetry is the control: the probe discriminates rather than matching everything, and a fabricated key is not found.

So icons do not tree-shake here and did not before. That is deliberate, not a defect: renderers/action/resolve-icon.ts imports the whole icons record because string lookups resolve against record MEMBERSHIP — the seam objectui#5935 consolidated onto and check-lucide-icon-record-names.mjs enforces. Moving that seam to lucide-react/dynamic.mjs to shed the bytes would resolve the very names the record deliberately drops (trash-2, edit, smile), which that gate's own header calls worse than having no gate. So there is no import-side fix available here that does not reverse a maintainer ruling.

The library is genuinely bigger, two independent ways:

  • Same-artwork control. pencil.mjs, whose icon node is byte-identical across the two versions, goes 455 to 522 bytes — because 1.43.0 reshaped every icon module into an __iconData object carrying name, size and aliases. The artwork is unchanged, so that delta is purely the reshape.
  • More icons. The whole icon directory, licence headers stripped and gzipped: 166,705 to 176,968 bytes (+10,263, +6.2%) from 1.31.0 to 1.43.0. Same order as the chunk delta.

So the ceiling moves, in the objectui#8888 / objectui#8901 shape

PER_CHUNK_GZIP_CEILINGS['ui-components'] 399,000 to 420,000, and PER_CHUNK_BASELINE['ui-components'] 391,095 to 410,904, together in one commit, with the reasoning written into the checker's own header as a Why ui-components moved UP section.

New headroom is 9,096 bytes = 0.10x REGRESSION_THIS_GATE_MUST_CATCH_BYTES — the proportion objectui#7399 re-pinned this key to, chosen over "just enough to pass", because a line left with almost no headroom is the defect objectui#8816 records and this key was the tightest of the four before the raise.

REGRESSION_THIS_GATE_MUST_CATCH_BYTES did not move. Nothing else moved: the aggregate was re-read on the same build and passes on its own — 3,589,068 of 3,597,000, headroom 7,932 bytes — so it is left alone.

Proof the raised gate can still fail

Both legs run the same --report invocation, so the red is attributable to the padding and not to the flag. The real report was never mutated — the padding went into a copy.

CONTROL (unpadded copy)   exit 0   ui-components 401.3 KB / 410.2 KB (headroom 8.9 KB)
PADDED  (+15,000 bytes)   exit 1   ui-components 415.9 KB / 410.2 KB (OVER by 5.8 KB)

pnpm exec vitest run scripts/__tests__/check-eager-closure-budget.test.ts — 104 passed.

What the bytes buy — and the part that makes this not optional

Roughly 16 KB gzipped on every console page load, for 51 new icons and a metadata reshape. On its own that is a thin trade.

But the published ranges are already ^1.31.0, which permits 1.43.0. Only this repository's lockfile is holding 1.31.0; anyone installing @object-ui/* fresh today already resolves 1.43.0, already pays these bytes, and already gets the blank DetailView delete icon that trash-2 now produces. This PR does not introduce that cost — it aligns the repository with what consumers already get, and repairs a glyph that is already broken for them.

If the trade still reads wrong, the lever is not this ceiling: it is pinning lucide-react to an exact version across the workspace, which is a policy change and a maintainer's call, not mine.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3504.9 KB 3512.7 KB
Main entry chunk (gzip) 144.1 KB 350 KB
Entry file index-B6n3sok8.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.20KB 114.67KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 28.04KB 9.46KB
data-objectstack (index.js) 201.97KB 56.22KB
fields (index.js) 246.97KB 62.30KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.54KB 46.34KB
plugin-dashboard (index.js) 132.41KB 34.84KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 253.19KB 65.62KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.96KB 40.93KB
plugin-grid (index.js) 210.86KB 57.28KB
plugin-kanban (index.js) 57.58KB 16.47KB
plugin-list (index.js) 112.54KB 27.65KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 83.34KB 27.61KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 2.71KB 1.35KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants