Skip to content

[finding] 28 published packages ship 36 .d.cts declarations (5.2 MiB) that no types condition points at — the same unreachable-published-dist class as #13013, measured 48x larger #13112

Description

@os-elon

Filed unassigned by the #13013 dev (PR pending), session session_01CPrUz21stTFhJRUirdc4yw. Observation class — nothing behaves wrongly. Not graded, not routed. Severity not judged.

Found while pruning dist/testing.cjs in #13013. It is the same defect class — bytes packed into the npm tarball that no exports condition can reach — but it is a different remedy with a different open question, so it was deliberately not fixed as a rider on that PR.

Measured

On a full pnpm build at 2fde235c84, comparing every emitted dist/**/*.d.cts against the union of declaration paths each manifest actually declares (types, typings, and every types condition at any depth inside exports):

publishable packages shipping UNDECLARED .d.cts: 28 · 36 file(s) · 5,497,247 B (5.2 MiB)
   1,144,571 B   1 file(s)  @objectstack/service-messaging
     959,102 B   1 file(s)  @objectstack/metadata-core
     559,196 B   1 file(s)  @objectstack/service-automation
     483,141 B   1 file(s)  @objectstack/service-storage
     462,256 B   1 file(s)  @objectstack/metadata-protocol
     234,012 B   2 file(s)  @objectstack/plugin-webhooks
     223,477 B   1 file(s)  @objectstack/runtime
     218,614 B   3 file(s)  @objectstack/lint

For scale: #13013 removed ~113 KB across two packages. This is ~48x that, and @objectstack/metadata-core alone carries more of it (959 KB) than #13013 removed in total.

Why they are unreachable

The dual-build manifests spell one types condition as a sibling of import/require rather than one inside each branch:

"exports": {
  ".": {
    "types": "./dist/index.d.ts",
    "import": "./dist/index.js",
    "require": "./dist/index.cjs"
  }
}

So both conditions resolve types to dist/index.d.ts, and the dist/index.d.cts twin that format: ['esm','cjs'] makes tsup emit is named by nothing. files: ["dist"] packs it anyway. check:published-files cannot object — it asks that the whitelist covers declared entry points, and it never reads dist/ at all (SKIP_DIRS includes it), so an extra shipped file is outside the question it asks.

Not established — and why this is not a mechanical prune

⛔ Do not read the measurement as "delete them". Two things need deciding first, and neither is settled by the numbers above:

  1. Whether any consumer toolchain reaches .d.cts by file adjacency rather than through the types condition. Unreachable-through-the-manifest is proven; unreachable-in-practice is not the same claim, and a require-side consumer resolving index.cjs is exactly where an adjacent index.d.cts would be looked for. That is a real question about published surface, not a build-config tidy-up.
  2. Whether the remedy is dropping the emission or declaring the file. The other direction is equally available and arguably more correct: give each condition its own nested types ("require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }), which makes the shipped file reachable instead of deleting it, and gives CJS consumers declarations built for CJS. Choosing wrongly here is a published-surface change in 28 packages at once.

Note the asymmetry with #13013: there the dead file had no possible reader, because the subpath had no require condition at all after #13001. Here there is a plausible reader and the question is which of two shapes is right.

The converse-gate question this sharpens

#13013 left open whether a gate should assert the converse of check:published-files — that every published entry-point-shaped dist file is reachable through exports — and warned the naive form would be noisy, because chunks, maps and .d.ts files are legitimately published and are not entry points.

This measurement suggests the noise objection is answerable, because the query above is already the precise form: compare emitted declaration files against the declared declaration set, which scripts/check-dts-emitted.mjs already computes (declaredDeclarationPaths) for the opposite direction. That gate asks "is every declared declaration present"; the converse asks "is every present declaration declared", over the same population, with the same function. Chunks and maps never enter it. Whether that gate should exist, and whether it reds or merely reports, is still open — but it would not be the noisy shape #13013 was worried about.

Dedup

One targeted search_issues (repo-scoped REST is 403 on this seat), 23 matches. Nothing open on undeclared .d.cts or on the converse reachability gate. Nearest neighbours, all distinct: #13013 (open, this card's parent class, testing.cjs only), #12985 (closed, created the ESM-only ./testing shape), #8133 (open, @objectstack/spec chunk sharing across entry bundles — a different property of a different package). Control: the same query returned #13013 itself, so the empty result is a reading rather than a broken instrument.

Re-check

git ls-files 'packages/**/package.json'

then, per manifest, diff dist/**/*.d.cts against the union of types, typings and every types condition inside exports. Requires a full pnpm build first — with no dist/ the population is empty and the query answers zero for the wrong reason.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions