Skip to content

fix(sync): a lazy route relinks a page that appears below its barrel - #2560

Merged
colbymchenry merged 1 commit into
mainfrom
claude/nested-barrel-sync
Oct 14, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/nested-barrel-sync

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Problem

A lazily loaded React Router route can name a barrel whose page sits in a folder below it:

// src/App.tsx (Arshia76/DigiShop.Frontend)
lazy: async () => { const { HomePage } = await import('@/pages'); return { Component: HomePage }; }

// src/pages/index.ts
import { HomePage } from './Home'   // src/pages/Home/index.tsx
export { HomePage, … }

A fresh index links this route to HomePage (#2558). codegraph sync and the file watcher don't, if the page arrives after the route: the file is added later, or an edit gives it the export the route reads. While the page is missing, the route's reference import:@/pages#HomePage fails and is parked under the key of the module it names, module:pages (#2422). Sync retries failed references by the keys of the files it changed, and src/pages/Home/index.tsx only offers its own folder's and stem's keys (module:Home, module:index). So the route stayed unlinked until pages/index.ts itself changed or the project was re-indexed. #2558 recorded this limit in framework-coverage.md, and #2436's export * barrels have it too.

Fix

reopenReferencesThroughBarrels in src/resolution/module-importers.ts runs right after the existing failed-ref retry and the added-files reopen (#2512). The retry has already made the barrel's import edge into the added file at that point.

  • Seeds. The files the sync added, plus the modified files that gained or lost a definition (definitionDelta). A body-only edit seeds nothing.
  • Walk. From each seed, follow the imports edges up to the importers that pass the module on. That means export … from / export *, or an import they export again: import { X } from './x'; export { X } or export default X. These are the forms exportedComponent follows (fix(react-router): a lazy route links the page a barrel module forwards #2436, fix(react-router): a lazy route follows a barrel that re-exports an import #2558). The walk goes as deep as the resolver chases a name (REEXPORT_MAX_DEPTH).
  • Reopen. The failed references waiting under those barrels' module keys go back to pending, and the sync's orphan sweep resolves them as a fresh index would.

Cost guards:

  • Only keys that some failed reference actually waits under are looked up. getFailedModuleKeys is one DISTINCT over the failed rows parked under module:* keys, served by a partial index: at most 0.17 ms on the six real indexes measured, Serviceos's 340k failed refs included. So a sync with nothing waiting reads nothing more.
  • The ImportReader is shared with the added-files reopen. Its new exportedBindings runs behind a regex pre-check, since most importers export nothing they import.

defaultExportBinding (the binding export default X; / export { X as default } hands over) moves to alias-binding.ts. The walk and react.ts's exportsBinding both use it, and export default withAuth(Page) is still followed nowhere.

Verification

  • Regression test __tests__/sync-nested-barrel-route.test.ts covers three barrel shapes:

    • an export … from barrel, @/pages → pages/home/index.ts → HomePage.tsx;
    • an import exported again, @/shop → shop/cart/index.tsx;
    • an export * chain three folders deep, @/admin → dashboard → panel → Dashboard.tsx.

    It has three cases: the pages appear (also compared with a fresh index), an edit gives the pages their export, and a page is deleted and comes back. All three fail on main (e30a2c1) and pass with the fix.

  • Mutations. Dropping the import-forwarding branch of passesOn loses /cart. Dropping the definition seeds fails only the edit case.

  • Suites. The related suites pass 355/355 (22 files). The full suite has 6,681 passing, 0 failing and 404 skipped.

  • Fresh index, 28 repos (bulletproof-react, milo-ui, outline, proshop, React Router's examples, DigiShop.Frontend, Serviceos and 21 more): dumps are byte-identical between main and the fix, so the exportsBinding refactor is behaviour-preserving.

  • Late-file sync, same 28 repos. Each run holds out every file a route links to in a fresh index, runs init, restores the files, runs sync, and compares with a fresh index of the same tree:

    main fix
    DigiShop.Frontend 13 routes left unlinked (import:@/pages#HomePage … #OrdersPage stay failed) synced graph equals a fresh index
    27 other repos — synced graph byte-identical to main's

    The 27 repos include outline, React Router's examples and Serviceos, whose synced graphs differ from a fresh index in the same way on both arms (30/31, 7/7 and 10201/10198 lines). That difference predates this PR.

  • Time of the new sync-barrel-modules phase (CODEGRAPH_SYNTH_TIMINGS=1): 0–6 ms on 22 repos, 46 ms on outline (187 files added at once), and 103 ms on Serviceos (258 added, nothing waiting under a barrel's key). Four more repos had no route target to hold out. The phase reopened 15 references on DigiShop and 1–12 on five other repos. Those failed again in the sweep, so their synced graphs equal main's.

Overlap

🤖 Generated with Claude Code

A lazy React Router route whose module is a barrel, and whose page sits in a
folder below the barrel's, stayed unlinked after `codegraph sync` added the
page or gave it its export, though a fresh index linked it. In
DigiShop.Frontend, `@/pages` is `src/pages/index.ts`, which imports `./Home`,
`src/pages/Home/index.tsx`, and exports it again. While the page is missing,
the route's reference fails and waits under the key of the module it names,
`module:pages` (#2422). The retry looks failed references up by the keys of
the changed files, and a file in `pages/Home/` gives only `module:Home` and
its own stem. The limit, which the `export *` barrels of #2436 share, was
recorded with #2558.

After the failed-ref retry, which makes the barrel's import edge into an
added file, sync now walks up the import edges from each file it added, or
gave a definition, to the barrels that pass it on: `export … from`, or an
import exported again (`import { X } from './x'; export { X }`, `export
default X`), as deep as the resolver chases a name. It re-opens, for the
sweep, the failed references waiting under those barrels' keys.

Only keys a failed reference waits under are looked up, in one DISTINCT
over the failed rows parked under module keys, so a sync with none reads
nothing, and a body-only edit seeds nothing. The ImportReader that the
added-files reopen (#2512) uses is shared, and gains exportedBindings,
behind a regex pre-check, since most importers export nothing.
defaultExportBinding moves to alias-binding.ts for this walk and for
react.ts's exportsBinding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit 9b23709 into main Oct 14, 2026
@colbymchenry
colbymchenry deleted the claude/nested-barrel-sync branch October 14, 2026 19:31
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.

1 participant