Skip to content

@objectstack/spec ships 18 self-contained entry bundles with no shared chunks, so a consumer mixing entry points gets different ConnectorSchema / defineConnector objects (measured) #8133

Description

@hotlong

Filing unassigned; found while implementing #7991 (PR #8129, the packages/qa/downstream-contract source alias). Deliberately not fixed there — that card's ruling is that a problem the dist boundary was hiding is a finding in its own right, and this one is about how packages/spec is built and published, which is a different blast radius from a QA harness alias.

The fact, measured against the published dist directly

Not through vitest, not through any alias — a bare import() of the built artifacts:

consta=awaitimport('./packages/spec/dist/index.mjs');constb=awaitimport('./packages/spec/dist/integration/index.mjs');a.defineConnector===b.defineConnector// falsea.ConnectorSchema===b.ConnectorSchema// false

Both specifiers are public: @objectstack/spec and @objectstack/spec/integration are entry points in the package's own exports map, and both export these symbols. A third party that imports one from the root and one from the namespace holds two different objects.

Why: the subpath bundles share nothing

Each entry bundle inlines its whole dependency graph. dist/integration/index.mjs opens with exactly one import, and it is external:

import{z}from'zod';

There is no shared internal chunk to import — so ConnectorSchema's definition is emitted independently into every entry bundle that reaches it. Measured on a fresh build:

  • 18 entry points in the exports map, each a self-contained bundle.
  • ConnectorSchema is defined in 5 of them (index, integration, kernel, system, cloud).
  • Largest bundles: system/index.mjs 1.9M, index.mjs 1.6M, kernel/index.mjs 1.4M, cloud/index.mjs 1.3M, api/index.mjs 1.2M.
  • packages/spec/dist totals 101M.

Impact — stated honestly, because I did not demonstrate a live break

What is measured is the duplication and the identity split. What I did not do is find a consumer that breaks on it today, so this should be graded rather than assumed:

  • Identity comparison across entry points is silently false. Anything keyed on schema identity — a Map or WeakMap keyed by a Zod schema, a registry that de-dupes by object reference, an instanceof-style check — gets two entries where the author expected one. This fails quietly; there is no error, just a lookup miss.
  • zod itself is external, so the usual worst case (two Zod runtimes disagreeing about what a ZodType is) does not apply here. That meaningfully lowers the severity and is why I am not filing this as a P0.
  • Payload duplication is real regardless of identity: a consumer importing three namespaces pulls three overlapping copies.

The reason it is worth a card even undemonstrated: this is the shape of bug that is invisible until it isn't, and #7991 is a fresh example of the same family — a property of dist that no test could see, being consumed as if it were a property of the source.

Note on how this surfaced

PR #8129 adds a pin case asserting defineConnector is one object across the root and the integration namespace. Aliased to source it passes (one tree, one copy); unaliased it fails. That pin's purpose there is to guard the alias and to stand watch for a dual instance, and it does both — but the underlying duplication it revealed belongs to the build, not to that harness, which is why it is filed here instead.

Fix directions (not chosen here)

  • A — enable code splitting for the multi-entry build so shared modules land in a common chunk that every entry imports. Restores identity and collapses the duplication; needs a check that the emitted chunk layout is compatible with the exports map and with CJS consumers.
  • B — accept the duplication and remove the ambiguity by making namespace subpaths re-export from the root bundle rather than rebuild it.
  • C — decide the identity guarantee is not offered and say so in the package docs, so nobody writes identity-keyed code against it.

A is the contract-first shape: the symbols are documented as the same exports, so they should be the same objects. Which of these is right is a maintainer call about how spec is published, which is why this is filed rather than patched.

Prior art (searched before filing)

Searched open issues for dual-instance / subpath-exports / duplicate-chunk / tsup-code-splitting phrasings against this repo; no open issue covers this. Related but distinct:


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