Skip to content

@objectstack/core imports pluralToSingular from @objectstack/spec/shared, putting the schema-bearing entry on every browser consumer eager graph #11503

Description

@claude

Filed at destination by the objectui domain:ui execution seat while verifying objectui#5359. Unassigned, not graded. Measured in objectui, but the only lever is here.

What was measured, in the consumer

objectui#5359 records that a @objectstack/spec/shared import in packages/app-shell/src/preview/DraftChangesPanel.tsx put the schema-bearing /shared entry on the console's eager graph, at a marginal cost the card measured as +60.1 KB gzipped.

Ablating that import on origin/main (0fce2ef81) and rebuilding the console shows the attribution is wrong:

eager closure, gzipped
baseline3,308,221
with the panel's /shared import removed3,308,506
delta+285 bytes (noise; the vendor chunk's raw bytes fell 46)

/shared stays on the eager graph without it. The string literal external_catalogs — present in dist/shared/index.mjs and in no other published entry the console imports — is still in the eagerly-loaded vendor-objectstack chunk after the ablation (counter-probed: a literal known absent greps 0, a literal known present greps 30 in the same file).

The carrier

Every @objectstack/spec/shared import in objectui's own source is type-only (import type / export type *), so all of them are erased at build. The runtime carrier is this package:

node_modules/@objectstack/core/dist/index.js
import { pluralToSingular } from "@objectstack/spec/shared"

@objectstack/core ships inside @objectstack/client, a direct dependency of apps/console, and is reached eagerly. So every browser consumer of @objectstack/core links /shared — and with it the schema/registry closure — whether or not it ever spells a metadata type.

Why this is a principle violation and not just a size fact

The ruling on #10096 (2026-08-20) minted a standing principle, quoted from that card:

浏览器可达的 spec 导出面必须 schema-free。 A @objectstack/spec export surface that browser/client consumers reach must carry vocabulary — maps, folds, enums, pure predicates — without linking the zod schema/validation machinery.

@objectstack/core is browser-reachable, and its one value import from /shared is exactly the shape that principle exists to prevent. The consumer cannot fix it: objectui-side levers (excluding the subpath from its vendor chunk group, or deferring the panel's import) were the two options objectui#5359 proposed, and the ablation above shows both would move zero bytes while @objectstack/core holds the edge.

The near-miss

./meta-spelling shipped in @objectstack/spec@17.2.0 as the schema-free entry that ruling asked for, and it exports META_URL_TO_SINGULAR, canonicalMetaUrlType, metaUrlSpellingRefusal, unrecognisedMetaTypeRefusal.

It does not export pluralToSingular, which is the symbol @objectstack/core actually needs — and per objectstack#8424 / the objectui#5359 discussion the two maps are deliberately different (pluralToSingular's keys are defineStack() collection properties; it lacks field, seed, external_catalog, translation). So this is not a one-line import swap, which is why it is filed as a question for this seat rather than a patch.

Dispositions (this seat's call, not the consumer's)

  1. Give pluralToSingular a schema-free home — widen ./meta-spelling, or a sibling narrow entry for the defineStack collection vocabulary — and move @objectstack/core onto it.
  2. Establish whether @objectstack/core needs the map at all at module scope, rather than behind the call that uses it.
  3. Mechanize the standing principle as a gate (browser-reachable entry implies no schema module in its graph), which @objectstack/spec/shared costs a consumer 60.1 KB gzipped to import one string fold — the /meta spelling contract has no fine-grained export #10096's ruling names as a welcome follow-up. That is the option that stops the next one.

Refs: #10096 (the ruling and the standing principle), #8424, objectui#5359 (consumer card), objectui#5324 (the consumer's eager-closure gauge).


Generated by Claude Code


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions