# One declaration of which entrypoints a browser may import

> Source: https://github.com/max-listov/stitchkit/blob/master/docs/decisions/0160-one-declaration-of-which-entrypoints-a-browser-may-import.md

---

# 0160 — One declaration of which entrypoints a browser may import

## Decision

`packages/core/entrypoints.mjs` declares every published entrypoint once:
subpath, source module, and whether the package promises a browser can import
it. The build, both browser gates, the reference-coverage walk, the guide table
and the consumer-lane matrix are all checked against it.

There is one `bun build` over every entry, not a browser lane and a server lane.

## What was actually wrong

Six places answered "is this browser-safe" independently: two build script
strings, the `exports` map, the guide's Use-in column, the consumer-lane
`target`, and the entrypoint map inside `reference-coverage.test.ts`. That test
already asserted three of them against each other, which is why the drift that
shipped was in the pair it could not see:

- `stitchkit/remote` was sold by the guide as "browser **and** server, stable"
  while it sat in the server build lane. No gate ever bundled it for a browser,
  so the promise was unchecked for as long as it existed. It happened to be true.
- `stitchkit/declaration` was built for the browser and exercised for Bun.

Both were found by a reviewer reading the lists side by side. Neither could have
been found by a gate, because there was nothing for a gate to compare against.

Moving `remote` into the browser lane fixed the build and the guide and **still
missed the matrix** — a three-place edit where the fourth place is silent is not
a fix, it is the same defect one step along.

## Why one build pass

The two lanes carried byte-identical flags and differed only in their entry
lists. The split bought nothing and cost the first of these two things. It made the
entry lists a second place to declare browser-safety — the thing this ADR exists
to end — and two `--splitting` runs into one `--outdir` produce two chunk
graphs, so `dist` shipped the contract layer more than once.

The size result is mixed and worth stating exactly, because the tempting summary
is wrong. The whole `dist` is **115 KB smaller**: `defineContract` lived in two
chunks and now lives in one. But a single browser entry's graph is **larger** —
one pass gives the bundler more entries to split across, so `index.js` reaches 14
files instead of 7, and its transitive payload grew about 8%. What a consumer's
own bundler does with that depends on their splitting, and we have not measured
it. The honest claim is deduplication of the published package, not a smaller
download.

It also repairs `dev`, which had never worked. `bun run build:js -- --watch`
appends the flag to the tail of the expanded chain, so `--watch` landed on a
post-processing script that does not read argv, and `bun build --watch` had never
run at all.

## What is asserted, and what is not

The guide's Use-in column is prose with eleven distinct values —
"server (Node ≥ 22.5)", "terminal (Bun)", "build and deployment tooling". A
boolean cannot generate that without deleting information, so the column is not
generated. It is asserted: an entry the manifest calls browser-safe must say
"browser" there, and one it does not must not.

The consumer-lane `target` is not the same predicate either — it has three
values and one subpath legitimately has several rows. So the assertion is the
weakest true one: a browser-safe entry must have **at least one** row that
bundles it for a browser. That is exactly the check `remote` failed.
