Skip to content

test(exports): close two holes in the exports-pairing guard - #777

Merged
sroussey merged 1 commit into
claude/wonderful-turing-rjtcnx-ai-typesfrom
claude/optimistic-goldberg-hxoj5s-exports-guard
Aug 14, 2026
Merged

test(exports): close two holes in the exports-pairing guard#777
sroussey merged 1 commit into
claude/wonderful-turing-rjtcnx-ai-typesfrom
claude/optimistic-goldberg-hxoj5s-exports-guard

Conversation

@sroussey

Copy link
Copy Markdown
Collaborator

Targets claude/wonderful-turing-rjtcnx-ai-types (#717), not main.

Two review findings, both the same shape: a guard test that does not guard what it claims. All changes are in packages/test/src/test/util/ExportTypesPairing.test.ts.

1. A browser block with types but no implementation defeated every check

collectBranches records a branch only when a string-valued implementation key is present, so a block like

"browser": { "types": "./dist/ai.browser.d.ts" }

produces no branch at all — it is invisible to all six rules, including the source-entry rule, which can only check targets a branch declared. Meanwhile Vite/webpack enter the browser condition, recognize no key in it, fall through to the outer "import", and bundle the node build, while tsc under customConditions: ["browser"] matches browser > types and type-checks against the browser declarations. That is the browser-typed-as-node inversion this file exists to prevent, reached by omission instead of by a wrong target.

browserSplitViolations now evaluates the empty-implementation case before the browser-source-entry guard, and the violation names the node target bundlers fall through to. Hoisting past if (!browserEntry.some(hasSourceEntry)) continue; is deliberate: the packages/* layout (stem node, no src/node.browser.ts) never reaches the guarded code, so an empty browser block there would carry the same bug unreported for every package in the repo.

Three fixtures: the openai manifest with its browser "import" deleted, the packages/storage layout with the never probe, and a healthy block that must stay silent. The first two assert findViolations and orderViolations are [] first — the go-red proof this file already uses.

2. "Source entry exists" proved only the .d.ts half

build-types runs tsgo over the package's whole src tree, so the existence of a source file is exactly what makes its declaration appear. The .js half comes from the hand-written entry lists in each package's build-code / build-browser / build-* scripts, which nothing read. Add providers/foo/src/ai.browser.ts and a browser block, forget to append ./src/ai.browser.ts to build-browser, and every check passed while dist/ai.browser.js was never emitted — the package publishes a browser condition pointing at a missing file.

New rule buildEntryViolations(manifest, exportsMap, scripts):

  • collects every .js/.cjs/.mjs implementation target,
  • derives its source entry from the dist stem, and
  • requires a whole-token match (^|\s)\.?/?src/<stem>\.tsx?(\s|$) in the joined text of every build* script.

Whole-token matching is load-bearing in both directions: bun build --outdir ./dist/storage ./src/storage/browser.ts must satisfy the nested stem storage/browser (duckdb, sqlite), and ./src/ai.browser.ts must not satisfy the stem ai — a substring test would pass a package that builds only its browser bundle while publishing the node one. Unrecognized layouts are reported, not skipped.

Verified over every workspace manifest before committing: zero violations, exactly one exemption. That exemption (packages/workglow, whose build-js is bun run build.ts) is pinned in GLOB_BUILT_PACKAGES and guarded by a test asserting the exempted package really does hand its build to a repo-local *.ts program — matched as the token right after bun/bun run, so a .ts handed to bun build as an entry does not satisfy it. The exemption therefore dies with the custom build rather than quietly exempting a hand-written list.

This is a rule in this test file rather than a prepack assertion in publish-workspaces.ts, on purpose: the defect is that a guard test does not guard what it claims, so it must be able to fail the PR that introduces it. A prepack assertion fires after review and after merge.

3. Comment correction

The comment above it("declares only targets a source entry file can emit") claimed the source entry is "the cheapest evidence that the build produces the target at all". It now says a source entry is necessary for either half and sufficient for only the declaration, and points at the rule that covers the other half.

Verification

  • bun scripts/test.ts util vitest — 54 files, 797 passed, 10 skipped.
  • ExportTypesPairing.test.ts alone: 40 passed (was 25).
  • Go-red: reverting only the browserSplitViolations hoist fails both new browser-split fixtures and nothing else.
  • Dry-ran the build-entry rule over all 38 manifests with exports: zero violations.
  • eslint clean, prettier --check clean on the changed file.

🤖 Generated with Claude Code


Generated by Claude Code

Both are cases where the guard test does not guard what it claims.

A `browser` block declaring only `types` was invisible to ALL SIX rules.
`collectBranches` records a branch only when a string-valued implementation
key is present, so such a block produces no branch at all — not even for
the source-entry rule. Bundlers, meanwhile, enter the `browser` condition,
match nothing, and fall through to the outer `import`, bundling the node
build while tsc (customConditions ["browser"]) types against the browser
declarations. That is exactly the browser-typed-as-node inversion this file
exists to prevent, reached by omission rather than by a wrong target.

`browserSplitViolations` now decides the empty-implementation case BEFORE
the browser-source-entry guard. Hoisting past that guard is deliberate: the
`packages/*` layout (stem `node`, no `src/node.browser.ts`) never reaches
the guarded code, so the bug would otherwise go unreported for every
package in the repo. Fixtures cover the openai manifest with its browser
`"import"` deleted, the `packages/storage` layout with the `never` probe,
and a healthy block that must stay silent — the first two assert
`findViolations` and `orderViolations` are `[]` first, the go-red proof.

"Source entry exists" proved only the `.d.ts` half. `build-types` runs
`tsgo` over the whole `src` tree, so the file's existence is what makes the
declaration appear; the `.js` comes from hand-written entry lists in each
package's `build*` scripts, which nothing read. Add
`providers/foo/src/ai.browser.ts` and a `browser` block, forget to append
`./src/ai.browser.ts` to `build-browser`, and every check passed while
`dist/ai.browser.js` was never emitted.

`buildEntryViolations` collects every `.js`/`.cjs`/`.mjs` implementation
target, derives its source entry from the dist stem, and requires a
whole-token match in the joined text of the package's `build*` scripts — so
duckdb's nested `--outdir` matches and `./src/ai.browser.ts` does not
satisfy the stem `ai`. Unrecognized layouts are reported, not skipped.
Zero violations across every manifest today, with exactly one exemption:
`packages/workglow`, whose build is glob-driven. That exemption is pinned
in `GLOB_BUILT_PACKAGES` and guarded by a test asserting the package really
does hand its build to a repo-local `*.ts` program, so it dies if the
package goes back to naming its entries.

This lives in the test file that reviews the manifest rather than in a
`publish-workspaces.ts` prepack assertion: a prepack assertion fires after
review and after merge, and a guard that does not guard has to be able to
fail the PR that introduces it.

Also corrects the comment above "declares only targets a source entry file
can emit", which claimed the source entry was the cheapest evidence the
build produces the target at all — true of the declaration only.
@github-actions

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 59.84% 37210 / 62176
🔵 Statements 59.35% 39044 / 65782
🔵 Functions 60.86% 7209 / 11844
🔵 Branches 48.06% 18902 / 39326
File CoverageNo changed files found.
Generated in workflow #3080 for commit e0a0b25 by the Vitest Coverage Report Action

@sroussey
sroussey merged commit d53f0ba into claude/wonderful-turing-rjtcnx-ai-types Aug 14, 2026
11 checks passed
@sroussey
sroussey deleted the claude/optimistic-goldberg-hxoj5s-exports-guard branch August 14, 2026 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants