Skip to content

fix(react-router): a lazy route follows a barrel that re-exports an import - #2558

Merged
colbymchenry merged 1 commit into
mainfrom
claude/barrel-import-forwarding
Oct 14, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/barrel-import-forwarding

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Problem

A lazily loaded folder whose index.ts passes on a page it imports linked nothing:

// Arshia76/DigiShop.Frontend, src/layout/index.ts
import { AdminLayout } from './AdminLayout'
import { UserLayout } from './UserLayout'

export { AdminLayout, UserLayout }
// src/App.tsx
lazy: async () => {
  const { UserLayout } = await import('@/layout')
  return { Component: UserLayout }
},

exportedComponent in src/resolution/frameworks/react.ts is the barrel walk behind lazy routes, React.lazy values and lazily loaded layouts (#2436, #2452, #2480). It reads the module's own export statement, then export … from and export *. For an export clause it looked for a declaration of that name in the barrel, but UserLayout there is an import binding, not a declaration, so it found nothing. The same happened for import Login from './Login'; export default Login, and for import { Page } from './page'; export { Page as Component }. All 27 lazy route refs in DigiShop's src/App.tsx stayed failed on main, and #2480's PR body listed this under "Not in this PR".

Fix

When the export names a binding with no declaration in the module, exportedComponent follows the module's import of that binding to the module it comes from. A default import continues as default, a named one by the name it imports. reads collects each file passed through, so lazyModules (#2452) still redraws the route when the page behind the import changes.

  • A namespace import (import * as Tabs …; export { Tabs }) is no component, so it links nothing.
  • A package's binding (import { Button } from '@mui/material'; export { Button }) doesn't resolve to a file, so it links nothing.
  • export default withAuth(Page) hands over a call, not a binding. ownExport reads its first identifier (withAuth). That was harmless while only same-file declarations counted, but following imports would link the route to the imported HOC. So an export default X is followed only when the statement is the binding itself (exportsBinding), and the default stays unlinked, as on main.

Verification

Regression test. __tests__/react-router-barrel-imports.test.ts has 9 tests:

  • 5 index cases: DigiShop's barrel, a barrel importing another barrel, export default of an imported binding, export { Page as Component }, and a React.lazy value through export { HelpPage as default }.
  • 2 negative tests: a namespace and a package's binding link nothing, and so does export default withAuth(AccountPage).
  • 2 sync cases: the page module appears later, and the imported page's default export moves.

On main, 7 of the 9 fail: every positive case and both sync cases. The 2 negative tests pass on main too, which is the point: they pin that the new step adds nothing there.

Checks.

A/B on real repos (fresh codegraph init, main 5da4a41d vs this branch; only react.js differs between the builds)

50 repos:

Results:

  • DigiShop.Frontend: 0 → 27 edges. These are the 13 pages behind @/pages and the 14 layout edges behind @/layout. Each lands on the declaration its barrel's import names; for example HomePage is in src/pages/Home/index.tsx, which src/pages/index.ts imports as { HomePage } from './Home'. No edge was retargeted or lost.
  • 49 repos are byte-identical.

Sync. On DigiShop:

  1. The 15 files the new edges land on were held out.
  2. The app was indexed, the files put back, and codegraph sync run.
  3. The 2 layouts relink: src/layout/UserLayout.tsx sits in the folder the route imports (@/layout), whose key the route's failed reference waits under.
  4. The 13 pages don't. Each sits one folder below its barrel (src/pages/Home/index.tsx behind @/pages), and module:pages is not a key a file in pages/Home/ gives.

That is the limit #2436 recorded for export * barrels; see "Not in this PR". A fresh index links those pages, and so does a later edit of src/pages/index.ts.

Not in this PR

  • A page added below its barrel's folder. A failed lazy-route reference is retried only by keys of the module it names, so a page that appears behind a nested barrel waits for a fresh index. The fix belongs in sync's failed-ref keying, for every barrel form, not in this walk.

Overlap

🤖 Generated with Claude Code

…mport

A lazily loaded folder whose index passes on a page it imports linked
nothing: DigiShop.Frontend's `src/layout/index.ts` does `import { UserLayout }
from './UserLayout'; export { UserLayout }`, and its routes load
`await import('@/layout')`. exportedComponent, the barrel walk behind lazy
routes, React.lazy values and lazy layouts (#2436, #2452, #2480), read the
export clause as naming a declaration in the barrel, which has none: the
name is an import binding. `import Login from './Login'; export default
Login` and `export { Page as Component }` of an imported Page failed the
same way, so all 27 lazy route refs in DigiShop's App.tsx stayed failed.

A binding the module exports but does not declare is now followed through
the module's import of it, a default import as `default` and a named one by
the name it imports, with each file read added to `reads` so lazyModules
still redraws the route. A namespace import or a package's binding links
nothing. `export default withAuth(Page)` hands over a call, whose first
identifier ownExport reads; that was harmless while only same-file
declarations counted, so a default is followed only when the statement is
the binding itself (exportsBinding), or the route would link the HOC.

#2451 (open) adds the same step to the general import walk; #2420 (open)
carried it in a lazyRouteComponent rewrite that exportedComponent superseded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit e30a2c1 into main Oct 14, 2026
@colbymchenry
colbymchenry deleted the claude/barrel-import-forwarding branch October 14, 2026 17:55
colbymchenry added a commit that referenced this pull request Oct 14, 2026
…2560)

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>
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