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>
malarinv pushed a commit to malarinv/codegraph that referenced this pull request Nov 7, 2026
…e or the first export

A module that exports an import of its own declares nothing under the
exported name: outline's `server/presenters/index.ts` is `import
presentUser from './user'; … export { presentUser, … }`, excalidraw's
`index.tsx` is `import Footer from "./components/footer/FooterCenter";
… export { Footer }`, and an `index.ts` beside a component is `import
Avatar from './Avatar'; export default Avatar`. findExportedSymbol found
no node there, so the import fell to a name match, which took any
namesake in the project. A default written as an expression had only the
first-exported-function guess behind it: `export default new Logger()`
sent outline's server logging calls to the browser app's Logger, and
`export default traceFunction({…})(accountProvisioner)` linked
`provisionFirstCollection`.

import-resolver.ts:
- statedDefaultExport also reads `new X()` (a class the file declares),
  what a wrapper call hands on (handedOn: a capitalized argument or the
  last of a curried chain, or a function written there, through nested
  wrappers; only a function, class or component of the file answers),
  and `export { X as default }`. A default naming a binding the file
  imports is that import, followed by esmDefaultExport with the walk's
  visited set and depth.
- A local export clause naming an import (`export { X }`, `export { X
  as Y }`, CommonJS `module.exports = { X }`) is followed too
  (forwardedExport; clauseImports is filled beside clauseAliases), in
  the named and the namespace-member branches.
- A default the statement leaves unnamed (`createStore(reducer)`) keeps
  the first-export guess, and a cast (`export default X as T`) is still
  not read.

Replaces colbymchenry#2451, whose JSX half colbymchenry#2563, colbymchenry#2558, colbymchenry#2738 and colbymchenry#2420 cover.

Regression test import-forwarding.test.ts: 6 cases, 5 fail on main (the
sixth guards `createStore(reducer)` and a call chained past `new X()`).
A/B on 16 repos, fresh init, each arm's kernel built from its own
sources: 9 byte-identical (5 controls); outline 448 retargeted, 348
added, 0 removed; react-native 71/7/0; excalidraw 4/2/0; mastodon
1/3/6; community-app 1/4/1; refine-examples 0/21/0; redux-realworld
0/9/0. Every changed edge triaged against the source.

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