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
colbymchenry
added a commit
that referenced
this pull request
Oct 10, 2026
#2436 lets a lazy route's module forward its component through barrels, so the answer reads every file that walk passes. lazyModules (was lazyModule) returns them all: a page added or edited behind an unchanged barrel moves the route on sync, as a fresh index does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry
added a commit
that referenced
this pull request
Oct 10, 2026
…e, and a new component gets its JSX edges (#2452) A route that renders `const Docs = lazy(() => import('./pages/Docs'))` binds to the component that module exports, and to the declaration while the module is missing or exports none (#2400's declaredComponent). The reference had resolved, so no failed-ref retry saw it, and CG-33's rebind reached it only when the module's component shared the declaration's name: a sync that added the module, gave it its default export or moved that export left the route on its old answer. A data router's `lazy` route kept a moved default export the same way. FrameworkResolver.lazyModules names the modules a route's answer reads, including the files a barrel forwards it through (#2436); React implements it for lazy-import references and the same-file lazy values Pattern 1 reads. After the CG-33 rebind, sync puts back the route references whose modules it added or changed (reopenRouteModuleReaders), in the order their edges were written, for the orphan sweep. The jsx-render refresh read only each changed file's own content, so a new component without JSX of its own never got the edge from `<Team />` in an unchanged file. A sync whose definition delta holds a JSX-renderable node now refreshes synthesis. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bompus
added a commit
to bompus/codegraph
that referenced
this pull request
Oct 10, 2026
…eceivers (#430) * fix(context): call paths label synthesized hops like explore, not as events (colbymchenry#2428) Two surfaces label a synthesized (dynamic-dispatch) `calls` hop: codegraph_explore's Flow and dynamic-dispatch links plus the codegraph_node trail (`dynamic: interface → impl @file:line`), and ContextBuilder's "## Call paths", which `codegraph context` prints (`→[…]`). Each kept its own list. The call paths' list stopped at callback, react-render, jsx-render, vue-handler, http-client, queue-job and socket messages, so every other synthesizer fell through to `event ${m.event ?? ''}`: an interface-impl hop read `→[event @src/pdf-exporter.ts:5]`, and so did a C++ override, a redux thunk, a gin middleware chain or a closure collection. The wording now lives once, in src/graph/synthesized-hop.ts (`describeSynthesizedHop`: a summary, the detail only the call paths print, and the wiring site), as AGENTS.md asks of a derivation two surfaces render. ToolHandler renders `dynamic: <summary> @site`, the call paths `<summary><detail> @site`. Explore and the node trail print exactly what they printed before; the call paths keep their wording for the kinds they already labelled, including the queue name, a socket's direction and the HTTP clause, and now name the rest the way explore does. A bus event reads `bus event` there too, and the section's footnote no longer says hops are marked `[callback/event …]`. The helper also holds the wording colbymchenry#2414 gives a Go method a struct gets by embedding (`metadata.promotedInto`), so that arm moves here too. The unused long `label` form of ToolHandler.synthEdgeNote (last read by the removed codegraph_trace) is dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sync): a route rendering a same-file lazy value follows its module, and a new component gets its JSX edges (colbymchenry#2452) A route that renders `const Docs = lazy(() => import('./pages/Docs'))` binds to the component that module exports, and to the declaration while the module is missing or exports none (colbymchenry#2400's declaredComponent). The reference had resolved, so no failed-ref retry saw it, and CG-33's rebind reached it only when the module's component shared the declaration's name: a sync that added the module, gave it its default export or moved that export left the route on its old answer. A data router's `lazy` route kept a moved default export the same way. FrameworkResolver.lazyModules names the modules a route's answer reads, including the files a barrel forwards it through (colbymchenry#2436); React implements it for lazy-import references and the same-file lazy values Pattern 1 reads. After the CG-33 rebind, sync puts back the route references whose modules it added or changed (reopenRouteModuleReaders), in the order their edges were written, for the orphan sweep. The jsx-render refresh read only each changed file's own content, so a new component without JSX of its own never got the edge from `<Team />` in an unchanged file. A sync whose definition delta holds a JSX-renderable node now refreshes synthesis. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cpp): a range-based for loop's variable has the type the loop declares (colbymchenry#2439) C++ receiver inference read a declaration only when the declared name was followed by `;`, `=`, `,`, `)`, `[`, `{` or `(`, so a range-based for's `Type name :` was never read. In protocolbuffers/protobuf's conformance runner, `suite->SetVerbose(…)`, `suite->RunSuite(…)` and `suite->GetFailureListFlagName()` inside `for (ConformanceTestSuite *suite : suites)` fell through to Strategy 3's guess by the receiver's name, and in RocksDB `log->file()` on a `log::Writer` loop variable went to `BlobLogWriter::file`. A line the declarator regex doesn't match is now read as a range-based for when its `for (` header declares the receiver before a single `:` (not `::`), and the loop's variable is taken only when the call is inside the loop's body: past the header, up to the `}` closing a braced body or the end of a single-statement body, with comments and literals skipped. After the loop, or in a later function, the same name is another variable, and the scan goes on as before. A loop over `auto` elements has nothing to read and keeps the existing fallback. Bit-fields and the scope operator are never read as a loop's declaration. A loop's declaration is recorded like any other (colbymchenry#2413's noteCppDeclaration), so the std-type gate applies to `for (const std::string& s : …)` too, and a loop that doesn't enclose the call still marks the receiver as shadowed. With colbymchenry#2421, the 13 conformance runner calls reach ConformanceTestSuite's methods at 0.9. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(js,ts): a default import is what `export default` declares, not the first exported function (colbymchenry#2432) A default import resolved to the module's first exported function or component whenever the `export default` statement wrote a declaration (`export default function Vans()`, `export default class Store`): only a separate `export default NAME` statement was read. A React Router 6.4+ data-router page exports its `loader` or `action` above the page component, so `import Vans, { loader as vansLoader } from './Vans'` bound the route `/vans` (and every call through `Vans`) to `loader`. The resolver now reads the file's own `export default` statement from comment-stripped code. A declaration it writes is found by position, a binding it names by name, and an anonymous function, class or arrow function is no node at all (the guess took an exported function beside it, or one nested inside it). The statement also beats an exported styled/memo component above it. Every line-initial statement is read and the first one that names a node of the file wins, so template text that only reads like a statement names nothing. Strings are not masked: a masker loses step on a template whose `${…}` holds another template or a regex with a backtick, and on outline it blanked the real statement below one. Only an expression default (`memo(Card)`, `new Service()`), a binding the file doesn't declare, or no statement keeps the old guess. `require('./x').default` (colbymchenry#2433) reads the same statement first, so it reaches the declared default too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cpp): a `->` call on a smart pointer or optional reaches the type it holds (colbymchenry#2440) A C++ call made with `->` on a receiver the calling function or its class declares as a `std::unique_ptr<T>`, `std::shared_ptr<T>` or `std::optional<T>` now resolves on `T`: on the class C++ name lookup finds for it where the call is written, or on a class that class derives from. Before, receiver inference read the type as `unique_ptr`, found no method, and the call fell through to the guess by method name: rocksdb's `std::unique_ptr<Iterator> iter; iter->Valid()` reached `ArenaWrappedDBIter::Valid`. The held type goes through colbymchenry#2407's lookup (`cppClassWritten`, extracted from `matchCppSupertype`), not `resolveMethodOnType` on its last name: rocksdb has 22 classes named `Iterator`, and the one the calls mean inherits its methods from `IteratorBase`, so a last-name lookup reaches `MemTableRep::Iterator`. The method is taken from that class or its bases through the class's own extends edges (`getSupertypeNodes`). A template parameter, a library type or an alias of one gets no edge; a type the project doesn't declare, or an alias the lookup can't follow, leaves the call as it was. The namespace-macro helpers move from name-matcher.ts to cpp-namespaces.ts unchanged, so name-matcher can import cpp-supertypes without a cycle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sync): a barrel edited to forward another page moves the routes that lazily load it --------- Co-authored-by: Colby Mchenry <me@colbymchenry.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry
added a commit
that referenced
this pull request
Oct 10, 2026
A data-router `lazy` loader resolves to the route's properties, and many
pick their page from the module they load: luci-go milo/ui's
`lazy: async () => { const { TestTab } = await import('…/tabs'); return
{ Component: TestTab }; }`, `const { default: Component } = await
import('./pages/Login')`, `import('./x').then((m) => ({ Component: m.Page }))`,
`({ Component: (await import('./x')).Page })`, React Router 7's
`lazy: { Component: async () => (await import('./x')).Page }`. scanRoutes
recorded only the first `import('…')` as `lazy-import:<spec>`, and
resolution linked the module's default export, else its `Component`: a
different page, or nothing (milo/ui's tabs barrel has neither).
lazyRouteReference reads the loader: its `Component`, or the page its
`element` shows, followed through the loader's bindings (destructured
`await import`, a module binding, `.then` callbacks, `Promise.all`, the
v7.5 object form), and the one lazily imported export a guard shows
(`<AgeGate><FireworksPage /></AgeGate>`). A loader that returns the module
keeps `lazy-import:<spec>`; one this does not read (a helper's, `.then(convert)`)
keeps its first import, as before; one that hands over only a `loader`
renders nothing. `async lazy() { … }` methods, which scanRoutes skipped as
entries, are read too, and comments no longer hide an import's specifier.
The picked export is named the way Vue Router and Angular name a lazy
component, `import:<path>#<export>`, so sync's module-tail retry (#2422)
parks and retries it unchanged, and it resolves through #2436's
exportedComponent, barrels included. React is registered before Vue Router
and Angular, so it answers only its own routes' references (tsx or jsx,
which theirs never are). lazyModules (#2452) names the files a picked
export is read from, so a sync that moves it redraws the route.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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