Skip to content

test(exports): catch a browser condition that names the node bundle - #756

Merged
sroussey merged 1 commit into
claude/wonderful-turing-rjtcnx-ai-typesfrom
claude/optimistic-goldberg-4xvngr-exports-browser-split-guard
Aug 13, 2026
Merged

test(exports): catch a browser condition that names the node bundle#756
sroussey merged 1 commit into
claude/wonderful-turing-rjtcnx-ai-typesfrom
claude/optimistic-goldberg-4xvngr-exports-browser-split-guard

Conversation

@sroussey

Copy link
Copy Markdown
Collaborator

Stacked on #717 (claude/wonderful-turing-rjtcnx-ai-types). Assertions only — no manifest is touched.

What breaks

packages/test/src/test/util/ExportTypesPairing.test.ts has six checks and every one of them is internally self-consistent: it compares a branch against itself (does this types name the implementation beside it, is it declared first, does a source entry exist for it, is it shadowed by a sibling). A browser block 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-ai and providers/tf-mediapipe carry exactly that duplicated shape today. They are correct as written and are deliberately left alone. Verified before writing any code:

  • providers/chrome-ai/src/ contains ai.ts, ai-runtime.ts, ai/ — and no ai.browser.ts;
  • providers/tf-mediapipe/src/ likewise.

So ./dist/ai.browser.js is 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) beside findViolations, with the source-entry probe injected so fixtures can drive both sides of it (the convention unresolvedWorkspaceMessage already uses). Per subpath:

  1. take the branch a plain node import resolves (nodeImportTargetnode/import/default only; browser, bun, react-native excluded on purpose, since the point is the target a browser split has to differ from);
  2. derive the stem with the existing distStem;
  3. probe 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 (stem node, no src/node.browser.ts) inert — that layout expresses its split as two peer entries, not a .browser suffix. Entry exists → require a browser condition 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: orderViolations

It treated only string-valued implementation keys as shadowers, so { "import": { default: … }, "browser": … } reads as two conditions and behaves as one dead branch, unflagged. Replaced with isShadowingImplementation, 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-wide orderViolations assertion stays []).

What the tests catch

  • New repo-wide test passing existsSync as the probe, asserting []. It is live, not vacuous: temporarily repointing providers/deepseek's browser target at ./dist/ai.js makes it fail with the expected message (verified, then reverted).
  • Fixtures proving it goes red only when the source exists:
    • chrome-ai's exact shape + probe true → the "not the browser bundle" message (the go-red proof; the same fixture is asserted [] against findViolations and orderViolations to show why a separate rule is needed);
    • chrome-ai's exact shape + probe false → [] (today's tree);
    • a correct provider shape (deepseek) + probe true → [];
    • same with the browser key deleted + probe true → the "no browser condition" message;
    • the packages/* browser/node layout + probe false → [] (no false positive).
  • Two fixtures for the shadowing predicate: a nested import that always resolves hides a later browser; 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

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
@github-actions

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 59.72% 37022 / 61992
🔵 Statements 59.23% 38840 / 65573
🔵 Functions 60.74% 7174 / 11810
🔵 Branches 47.89% 18767 / 39184
File CoverageNo changed files found.
Generated in workflow #3029 for commit c6906ca by the Vitest Coverage Report Action

@sroussey
sroussey merged commit aa94f1f into claude/wonderful-turing-rjtcnx-ai-types Aug 13, 2026
11 checks passed
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