Repository navigation
fix(react-router): a lazy route links the page a barrel module forwards - #2436
Merged
Merged
Conversation
A data router's `lazy: () => import('@/pages/invocation_page')` loads the
folder's `index.ts`. When that file only forwards the page, with
`export * from './invocation_page'`, `export { default } from './page'` or
`export { Page as Component } from './page'`, the route linked nothing: the
module was read for an export of its own and none was found. The lookup now
follows the module's re-exports a few hops to the file that declares the
page, the way JavaScript resolves them (`export *` never forwards the
default, a name two `export *` modules both forward is exported by
neither), and reads a module's own `export { Page as Component }` clause.
`React.lazy` values in a route file, which go through the same lookup,
follow such barrels too.
On luci-go's milo/ui, 51 of 91 failed lazy route refs now link; the rest
load a file under `src/build/` (not indexed), export only a `loader` or a
`handle`, or pick a named export in an async loader.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pins the sync side of the barrel fix: a page added behind its folder's index file after the index, and an edit that gives the page the `Component` its barrel forwards, both link on `sync` the way a fresh index links them. Both fail with sync's failed-reference retry stubbed out. Adds the CHANGELOG entry and the validation note in the framework coverage doc. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…aa039 # Conflicts: # CHANGELOG.md
This was referenced Oct 7, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A React Router data-router route
lazy: () => import('@/test_verdict/pages/invocation_page')linked nothing when the module it loads is a barrel:invocation_page/index.tsholdingexport * from './invocation_page'. That holds even on a fresh full index.lazyRouteComponentinsrc/resolution/frameworks/react.tsresolved the spec to one file and read only that file, forexport default …orexport (const|function|class) Component. Three forwarding forms were never followed:export * from './x'export { default } from './x'export { Page as Component } from './x'The same reader also missed a module's own export clause,
export { GraphView as Component };(milo/ui's chronicle views, folo, blog-frontend).Found on luci-go's milo/ui: 91 failed
lazy-import:/layout:lazy-import:refs over 51 modules.Fix
lazyRouteComponentnow asksexportedComponent(file, 'default'), thenexportedComponent(file, 'Component'). That walk:export default Page,export function/const Component, orexport { Page as Component }viaextractLocalExportAliases;context.getReExports, up to 4 hops with a visited set, the wayexportedTablein the same file already follows them for route tables.It follows JavaScript's rules, so it never guesses:
export *never forwards the default.export *would forward.export *modules both forward, to different declarations, is exported by neither, so nothing is linked.React.lazyvalues in a route file (declaredComponent) andlayout:refs throughimport { default as X }go through the same function, so they follow barrels too.Verification
Regression test.
__tests__/react-router-lazy-barrel.test.ts:react.ts, 6 of its 7 index cases fail. The 7th, the negatives, passes on main and guards against over-linking.export *skips the default" rule each fails that negative case. Both mutations were checked.Component. Both fail whengetRetryableFailedReferencesis stubbed to return nothing.Checks.
tsc --noEmitis clean, and so is a strict type-check of the new test file. The 20 React, React Router and router test files pass; 8 of them hit hook timeouts on this saturated box and passed when rerun serially. Full suite on a box at 100% CPU: 6,183 passed and 5 failed. Rerun serially, the 9 files involved pass except function-ref’s #1820 case, which hits its own 60 s timeout on main too.A/B on real repos (fresh
codegraph init, mained199e60vs this branch)21 repos. Only
react.jsdiffers between the two builds.export { default as Component } from)export { App as Component })const Component = observer(…); export { Component })export { RetroPage as default } from)The 16 controls are bulletproof, rr-examples, infrahub, outline, medusa, berry, mantis, matx, minimal-kit, hathor-explorer, bitser, flight-planner, proshop, next-saas, t3-turbo and takenote.
Every added edge was checked by hand. On milo/ui each barrel reaches the leaf module's
export function Component, chronicle's five views reach the view their clause names, and fleet's home page reachesHomePage. The added sets are identical on the earlier base22a7ba85. After the merge I re-ran it on final main51000e3a, which by then included #2433, #2437 and #2442 (JS/TS resolution): the same 60 added edges, and bulletproof, rr-examples, infrahub, mantis, proshop and next-saas byte-identical.milo/ui's other 40 failed refs are not this bug:
src/build/. The default ignore skipsbuild/at any depth, so that whole feature folder is unindexed.loader.@/fleet/root, which exports onlyhandle = { layout }.lazy: async () => { const { TestTab } = await import('…/tabs'); return { Component: TestTab }; }, where the loader picks a named export.Medusa's admin dashboard. Each of its 193 route-map modules is an
index.tsthat forwards the page, mostly asexport { X as Component } from './x'. Its routes aren't extracted (const routes = getRouteMap({...}); createBrowserRouter(routes)isn't followed), so the A/B can't show them. Calling the resolver on each spec directly: main links 0 of 193 and this branch links 193 of 193. An independent text oracle that follows the barrels by hand agrees on all 187 it can decide. The other 6 re-export with an explicit.tsxextension, and I checked those by hand.Sync. I held out every file a lazy route links to, indexed the rest, put the files back and ran
codegraph sync. On parv-portfolio, blog-frontend, article-app and folo the synced graph equals the fresh index. On milo/ui it differs by 4 test-fileimportsedges, and main differs by the same 4.Known sync limit. A page nested two barrels deep in a folder named unlike the route's import isn't relinked when it's added or edited after the index. For example,
@/builds/pages→pages/index.ts→builder_list_page/index.ts→builder_list_page.tsx. The cause:moduleReferenceKeysonly covers the changed file's own folder and stem. A fresh index links it, and before this change neither did. In the late-file runs above, every page sat in its barrel's folder or was named like the route's import, so all of them converged.Overlap with #2420
#2420, still open, also rewrites
lazyRouteComponent. It returns aNode, takes amember, and followsimport X from './X'; export default Xplus named re-exports. Its wildcard step runs only for a non-default member, so it does not fix these routes: a data-router lazy module goes through the default member, andexport *has no default. Whichever lands second should keep #2420's structure and add this PR's step: when the default finds nothing, look upComponent, followingexport *with the ambiguity rule. Both PRs' tests pin the behaviour.Not in this PR (follow-ups)
src/build/source folders in JS/TS apps are skipped by the default ignore. That is milo/ui's wholebuildfeature.createBrowserRouter(routes). That is all of Medusa's admin routes.lazyloaders that pick a member,{ default: Component }or{ TestTab }. The ref records only the module, so such a route links the module's default orComponentrather than the picked export.🤖 Generated with Claude Code