test(exports): catch a browser condition that names the node bundle - #756
Merged
Conversation
Every check in ExportTypesPairing is internally self-consistent: it
compares a branch against itself. A `browser` block that duplicates the
default branch therefore passes all six while routing browser consumers
to the NODE bundle.
Adds `browserSplitViolations`, keyed on the one piece of evidence that
tells a legitimate duplicate from a bug — whether a `<stem>.browser.ts`
source entry exists beside it. The probe is injected so fixtures drive
both sides. chrome-ai and tf-mediapipe have no such entry, so their
duplicate blocks stay green and are left untouched; the rule fires the
moment someone adds the source entry.
Also widens `orderViolations`: it counted only string-valued
implementation keys as shadowers, so a nested `"import": { default: … }`
could hide a later `"browser"` unflagged. An object counts when it
resolves unconditionally; one with no unconditional target does not,
because Node falls through it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomTUtZSTgUbFCYqFs4pcu
Coverage Report
File CoverageNo changed files found. |
sroussey
merged commit Aug 13, 2026
aa94f1f
into
claude/wonderful-turing-rjtcnx-ai-types
11 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #717 (
claude/wonderful-turing-rjtcnx-ai-types). Assertions only — no manifest is touched.What breaks
packages/test/src/test/util/ExportTypesPairing.test.tshas six checks and every one of them is internally self-consistent: it compares a branch against itself (does thistypesname the implementation beside it, is it declared first, does a source entry exist for it, is it shadowed by a sibling). Abrowserblock that is a byte-for-byte copy of the default branch satisfies all six — and routes browser consumers to the node bundle. That is the exact class of bug the file was written to prevent, expressed as a duplicate rather than as a wrong target.What was NOT changed, and why
providers/chrome-aiandproviders/tf-mediapipecarry exactly that duplicated shape today. They are correct as written and are deliberately left alone. Verified before writing any code:providers/chrome-ai/src/containsai.ts,ai-runtime.ts,ai/— and noai.browser.ts;providers/tf-mediapipe/src/likewise.So
./dist/ai.browser.jsis a bundle nothing builds. Repointing either manifest at it would immediately turn this PR's own existing "declares only targets a source entry file can emit" test red. Both are browser-only packages with one entry each; the two bundles genuinely are the same file. Their blocks are inert today, and the new rule fires the instant someone adds the source entry.The fix
A pure
browserSplitViolations(manifest, exportsMap, hasSourceEntry)besidefindViolations, with the source-entry probe injected so fixtures can drive both sides of it (the conventionunresolvedWorkspaceMessagealready uses). Per subpath:importresolves (nodeImportTarget—node/import/defaultonly;browser,bun,react-nativeexcluded on purpose, since the point is the target a browser split has to differ from);distStem;dirname(manifest)/src/<stem>.browser.{ts,tsx}.No source entry → skip. That is what keeps chrome-ai green and what keeps the
packages/*browser/node split (stemnode, nosrc/node.browser.ts) inert — that layout expresses its split as two peer entries, not a.browsersuffix. Entry exists → require abrowsercondition and that every implementation string inside it is./dist/<stem>.browser.js. Two distinct messages: no browser condition declared, and browser condition names the node bundle.Also:
orderViolationsIt treated only string-valued implementation keys as shadowers, so
{ "import": { default: … }, "browser": … }reads as two conditions and behaves as one dead branch, unflagged. Replaced withisShadowingImplementation, which also counts an object whose implementation keys resolve unconditionally. A nested object with no unconditional target ({ import: { node: … } }) does not shadow — Node falls through it. String behavior is unchanged, and no existing manifest newly violates (the repo-wideorderViolationsassertion stays[]).What the tests catch
existsSyncas the probe, asserting[]. It is live, not vacuous: temporarily repointingproviders/deepseek's browser target at./dist/ai.jsmakes it fail with the expected message (verified, then reverted).[]againstfindViolationsandorderViolationsto show why a separate rule is needed);[](today's tree);[];browserkey deleted + probe true → the "no browser condition" message;packages/*browser/node layout + probe false →[](no false positive).importthat always resolves hides a laterbrowser; one that does not, does not.Verification
npx vitest run --project test packages/test/src/test/util/ExportTypesPairing.test.ts— 28 passed.bun scripts/test.ts util unit vitest— 54 files, 785 passed / 10 skipped.npx tsgo -p packages/test/tsconfig.json— clean.Generated by Claude Code