Skip to content

finding(app-shell/console): the DraftChangesPanel /meta spelling fold puts @objectstack/spec/shared on the console's EAGER graph — +60.1 KB gzipped that no budget weighs #5359

Description

@os-support-ai

Recorded while implementing #5356 (PR pending). Not a defect in that change — the import there is the sanctioned form and the alternative is refused by a gate (below). This card exists so the bytes are not invisible.

What was measured

packages/app-shell/src/preview/DraftChangesPanel.tsx now imports canonicalMetaUrlType from @objectstack/spec/shared to fold a stored metadata type to its canonical singular before it reaches a /meta item route.

Marginal cost of adding that subpath to a graph that already holds the @objectstack/spec entries app-shell imports statically (/ui, /kernel), measured with the repo's esbuild (--bundle --minify --format=esm --platform=browser), the same method postureHasOrgWall's comment in packages/app-shell/src/hooks/useTenancyPosture.ts used:

entry graphminifiedgzipped
spec/ui + spec/kernel1289.8 KB342.6 KB
the same + spec/shared1503.2 KB402.7 KB
marginal+213.4 KB+60.1 KB

Mechanism: every published spec subpath is a self-contained bundle (dist/ui/index.mjs inlines its dependencies rather than importing a shared chunk), so /shared re-ships registry and schema modules the other entries already carry, and nothing tree-shakes them — @objectstack/spec declares no sideEffects, and shared/index.mjs runs assertMetaUrlSpellingsAgree() at module load, which pins META_URL_TO_SINGULAR to DEFAULT_METADATA_TYPE_REGISTRY.

Why the bytes are eager, and why laziness cannot move them

The panel is reached through static imports only: console/AppContent.tsxlayout/ConsoleLayout.tsxpreview/DraftPreviewBar.tsxpreview/DraftChangesPanel.tsx.

An await import('@objectstack/spec/shared') would not help. apps/console/vite.config.ts's vendor-objectstackadvancedChunks group claims every @objectstack/* module except @objectstack/lint (VENDOR_OBJECTSTACK_TEST), and that group's chunk is a static import of the app entry — so a dynamically reached spec module is folded into the eager closure exactly as objectui#5266 documents for the linter.

Nothing fails today: the console's performance budget weighs the index-*.js entry chunk alone and these bytes land in a vendor chunk — the blindness #5324 is about. assertLazyLinterStaysLazy is unaffected (it asserts about @objectstack/lint, and its counter-probe requires an eager spec chunk).

Why the panel does not just keep a local copy of the 35-entry table

Tried first, and refused mechanically: node scripts/check-spec-symbol-derivation.mjs flags a local META_URL_TO_SINGULAR / canonicalMetaUrlType as spec-named symbols that are hand-written rather than derived, and its own header explains that a faithful copy is precisely the fork the guard exists to prevent (reference identity is the only thing that distinguishes a re-export from a fork). The ALLOW map governs deliberate divergence, which a mirror is not. So the import is the correct spelling for #5356; the weight is the cost of it.

The lever, if this is worth paying down

Both halves live outside #5356's file surface (apps/console/vite.config.ts), which is why this is a separate card:

  1. exclude the @objectstack/specshared subpath from VENDOR_OBJECTSTACK_TEST, the way @objectstack/lint is excluded, and
  2. switch the panel's fold to await import('@objectstack/spec/shared') inside the async boundary that already exists (listPendingDrafts), with a build-time assertion in the spirit of assertLazyLinterStaysLazy so it cannot silently become eager again.

Worth weighing against the failure mode it introduces (a chunk-load failure would then break the panel's list) and against the upstream alternative: a finer-grained spec export for the /meta spelling contract, which would make this whole trade disappear.

Related: #5324 (the budget cannot see these bytes), #5325 (dynamic imports defeated by a static import of the same module), objectui#5266 (the linter's eager-vendor-chunk precedent).

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions