Skip to content

fix(sync): a barrel whose re-export points elsewhere re-links its importers - #3059

Merged
colbymchenry merged 1 commit into
mainfrom
claude/pr-2985-barrel-retarget-sync
Oct 30, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/pr-2985-barrel-retarget-sync

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

codegraph sync and the file watcher now re-link the files importing through a barrel when an edit points one of its re-exports at another module (export { foo } from './a' changed to './b', an export * moved the same way), takes one away, or drops a name from one, in JavaScript and TypeScript.

Root cause

c.ts imports foo through index.ts, which re-exports it from a.ts. Change the clause to './b' and sync: bar kept calling a.ts:foo, where a fresh index gives b.ts:foo. The barrel declares nothing of foo, so the definition delta has nothing to rebind; no file is added, so neither the walks below a barrel nor the added-files pass run; the binding never failed, so the failed-reference retries never see it; and the name-matched-import pass reads only bindings a name match took.

What came from the contributor, and what we changed

From @danusha2345 (PR #2985), unchanged: reopenReferencesThroughRetargetedModules in module-importers.ts, the importer walk the removed-files pass uses, which re-opens each importer's bindings through the changed barrel (and up through barrels passing it on), the failures parked under them, and the edges into the members of what the bindings pointed at (w.run() with w: Widget); the call after the name-matched-import pass; their 7 tests.

Our changes on top:

  • The trigger compares the export … from statements themselves. The original compared the set of modules a file imports and also counted any module that lost an import, so a plain import edit in a widely imported module re-opened every binding of all its importers: dropping one import type line from excalidraw's packages/excalidraw/types.ts (218 importers, no re-exports) re-opened 2,186 refs for the same graph. Import nodes carry their statement as the signature in both engines, so the orchestrator now samples each file's export … import nodes before and after the store (getReExportStatementsByFiles); that edit now re-opens nothing. Comparing statements also catches a name dropped from a clause whose module stays (export { foo, qux } from './a' to export { foo } from './a'), which diverged on main and with the original trigger. The import-then-export forms (import { foo } from './a'; export { foo }, renamed, namespace, a forwarded default) already converge on main.
  • Two tests for those (a dropped name; a plain-import edit reports nothing).
  • The doc comments the new code had been inserted between (SyncResult, getNodeNamePairsByFiles) moved back onto their declarations.
  • The CHANGELOG bullet moved beside the barrel-edit one (fix(sync): a name imported through a barrel links when an edit gives the barrel its re-export #2744), in the house format, with credit.

Validation

  • Tests: 9 in __tests__/sync-barrel-retarget.test.ts. On main's source 7 fail (the body-only and plain-import controls pass); on the contributor's original trigger the 2 new ones fail; all 9 pass here.
  • Sync against a fresh index (nodes and edges as sets) over 15 fixture edits, each in a later process (codegraph sync) and in the same process (one CodeGraph handle, as the watcher and MCP server): main 80f6b50 diverges in 8 of 15 in each mode, this branch in none. The edits: named, export *, export * as lib and renamed re-exports retargeted; a scoped sync of the barrel alone; a re-export taken away; a second barrel passing the first on; a value typed by the binding (new Widget(); w.run()); a name dropped from a clause; four import-then-export forms; a plain import dropped; a body-only control.
  • Real repo: excalidraw e7b53466, export * from "./utils" in packages/math/src/index.ts (116 importers through @excalidraw/math) retargeted to ./constants: main's sync is 224 edges off a fresh index (126 stale, 98 missing); here identical over nodes, edges, refs and files, with 3,216 refs re-opened (222 ms for the pass). Fresh indexes are identical to main's (sync-only change).
  • Cost: a plain one-file body-edit sync on excalidraw, median of 9 interleaved runs on main 80f6b50: main 4,077 ms, this 4,022 ms. The added work on every sync is one query over the changed files' export … import nodes before and after the store; the walk runs only for a module whose re-export statements changed.
  • Full suite on the pushed commit (CODEGRAPH_KERNEL_EXPECT=1, CODEGRAPH_NO_DAEMON unset, 4 workers, on main 80f6b50): 803 files passed, 6 skipped; 9,576 tests passed, 86 skipped, 0 failed.

Fixes #2978

Lands #2985 by @danusha2345.

🤖 Generated with Claude Code

…orters

`c.ts` imports `foo` through `index.ts`, which re-exports it from `a.ts`.
Change the clause to `./b` and sync, and `bar` kept calling `a.ts:foo`
where a fresh index gives `b.ts:foo`. The barrel declares nothing of
`foo`, so the definition delta has nothing to rebind; no file was added
for the walks below a barrel or the added-files pass; the binding never
failed, so the retries never see it; and the name-matched-import pass
reads only bindings a name match took. `export *` moved the same way, and
a scoped sync of the barrel alone too.

The orchestrator now reports the modified ES modules whose `export … from`
statements changed, read off their import nodes (named by the module, with
the statement as signature) before and after the store. Their importers'
bindings through them go back for the sweep, up through the modules that
pass them on in turn, with the failures parked under them and the edges
into the members of what the bindings pointed at (`w.run()` with
`w: Widget`). It is the importer walk the removed-files pass uses, run
after the name-matched-import pass.

Review changes: the trigger compares the `export … from` statements
themselves. The original compared the modules a file imports and counted a
module that lost any import, so dropping a plain import from excalidraw's
`types.ts` (218 importers, no re-exports) re-opened 2,186 refs for the same
graph; it now re-opens none. Comparing statements also catches a name
dropped from a clause whose module stays (`export { foo, qux } from './a'`
to `export { foo } from './a'`), which the module set missed. Plus tests
for both, and the doc comments the new code sat between moved back.

Validation: 9 tests; on main's source 7 fail (the 2 controls pass), and
on the original trigger the 2 new ones fail. 15 fixture edits, each
compared with a fresh index, in a later process and in the same process:
main diverged in 8 of each, this in none. excalidraw with
`export * from "./utils"` in its math barrel (116 importers) retargeted to
`./constants`: main was 224 edges off, identical here over nodes, edges,
refs and files. Fresh indexes are identical to main's.

Original PR #2985 by @danusha2345.
Fixes #2978

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

sync: retargeting a barrel's re-export (export { foo } from './a' -> './b') leaves importers on the old module

1 participant