Repository navigation
fix(sync): a route rendering a same-file lazy value follows its module, and a new component gets its JSX edges - #2452
Open
colbymchenry wants to merge 4 commits into
Open
colbymchenry wants to merge 4 commits into
colbymchenry wants to merge 4 commits into
Conversation
…e, and a new component gets its JSX edges
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.lazyModule names the module a route's answer reads; 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 module it added or changed (reopenRouteModuleReaders), 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>
…-fc6ebc # Conflicts: # CHANGELOG.md # docs/design/framework-coverage.md # src/resolution/types.ts
…ere written Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-fc6ebc # Conflicts: # CHANGELOG.md
This branch has not been deployed
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.
codegraph syncand the file watcher left a React Router route on a stale answer when the router file loads the page lazily into a value of its own and the page's module changes after the router was indexed. A sync that added a component with no JSX of its own also never drew the JSX edges to it. A fresh index of the same files gets both right.Repro
package.jsondepends on react and react-router-dom, andsrc/App.tsxis:Index it without
src/pages/, then addsrc/pages/Team.tsx(export default function Team() { return null; }) andsrc/pages/Docs.tsx(export default function DocsPage() { return null; }), and runcodegraph sync. Compared with a fresh index of the same files in another folder:/docsstays onconstant Docs(exact-match). A fresh index binds it tofunction DocsPage(framework).App → Team(jsx-render) is missing./teamconverges only by luck: CG-33's rebind re-opens edges by the target's name, and the page's component is namedTeamlike the declaration.Cause
The route. A route that renders a same-file lazy value binds, through
declaredComponent(#2400), to the component that value's module exports. While the module is missing or exports none, it binds to the declaration instead. The reference resolved either way, so:The same staleness follows an edit that gives the module its default export, or moves it to another component while the old one stays: the route's edge follows its old target through the module's re-index. That last case also holds for a data router's
lazy: () => import('./x')route.The JSX edge.
jsx-renderedges are redrawn only when a changed file passes the synthesis content gate (hasSynthesisPattern), or had synthesized edges touching it. A new component file with no markup of its own (return null, a wrapper, astyledcomponent in a.tsfile) passes neither, so<Team />in an unchanged file never got its edge. This half isn't specific to lazy routes: a statically imported<Team />behaves the same.Fix
A new
FrameworkResolver.lazyModules(ref, context)hook. It returns the modules whose contentresolve()reads to answer a route's reference. React implements it for:lazy-import:references, plain or behindlayout:;declaredComponentabout. Those are a route's PascalCase component in a.tsx/.jsxroute file, which its own file declares as a lazy value.The list includes every file #2436's barrel walk passes through on the way to the component: the walk (
exportedComponent) now records what it reads.declaredComponent's loader reading moved intodeclaredLoader, and the module path intolazyModuleFile, so the hook and resolution read the same files.ReferenceResolver.reopenRouteModuleReaders(changedFilePaths). Sync calls it right after CG-33's rebind, so the orphan sweep resolves what it puts back.getRouteEdgesMovedBy): edges from a route in an unchanged file whose target is either a node of a changed file or a constant or variable beside the route. They come back in the order they were written.lazyModulesinclude a changed file, it turns the edge back into the reference that made it, as the rebind does.refNamestamp and synthesized edges are never touched.A synthesis refresh for a new component. After the store phase, a sync whose definition delta includes a JSX-renderable node in a changed script also refreshes synthesis. That means a component, function or class with a tag's capital, or any
component(isJsxChildCandidate, next tojsxChild).Validation
mainmoved four times while this was validated:import { default as X }is the module's default import #2412 and others);require('./x').defaultreaches an ES module's default export #2433, fix(c,cpp): a file named like a Google unittest, foo_unittest.cc, is a test #2434, fix(js,ts): a catch-all or node_modules path alias no longer makes package imports look local #2437, fix(go): a defined type links the types it is defined from #2441, fix(go): a call through a type assertion reaches the asserted type's method #2444, fix(go): a dotted call is written through its own receiver, and a local named like an import is that variable #2448, fix(cpp): a class keeps its members past attribute macros the parser can't read #2449).Each step below names its base. The convergence checks were repeated on each base.
Tests.
__tests__/sync-lazy-route-value.test.tshas 12 tests. Each sync is compared, whole graph and edge metadata included, with a fresh index in a separate folder. The cases:Loadable(lazy(() => import(…)))in a route table another file hands the router;lazyroute whose module switches its default export;Two guards: a sync of an unrelated file re-opens nothing, and a new PascalCase constant or lowercase function doesn't refresh synthesis.
Red phase.
main(e0bbb66, and again on ed199e6), 10 of the first 11 fail. One of the 10 fails only because it spies on the new method. The precision guard passes on both.Each half has teeth. With the re-open disabled, the 6 route-convergence cases fail. With the synthesis trigger disabled, the 2 JSX cases fail, plus the 2 route cases that also check
App → Team.On the final merge (16344af):
tscis clean, and these pass:react-router-nested-lazy(3; itsbeforeAlltimed out once under load and passed alone);require('./x').defaultreaches an ES module's default export #2433, fix(js,ts): a catch-all or node_modules path alias no longer makes package imports look local #2437 and perf(resolution): read a bare-recorded call's receiver on its own line first — graph byte-identical #2431 added, which the merge script runs before pushing.The scenario matrix and the CLI repro converge.
On 883504d:
tscis clean.jsx-child-disambiguation,jsx-render-work); fix(sync): a route links the module it lazily loads when the module appears later #2422's and fix(sync): a navigation call links its route when the route appears later #2425's sync retries;sync;sync-rebuild-convergence; React Router; HOC components; and fix(go): a call on an embedded struct's method does not reach the embedder's method of that name #2430's Go test.cli-sync, every router, JSX disambiguation,import-default-as, the framework language gate and frameworks-integration.Full suite. One full-suite run was made, on e0bbb66 plus this fix. This box was saturated (100% CPU, about 106 node processes from other sessions), and 721 of 6,534 tests failed:
mcp-daemonpasses 22/22. Inmcp-projectpath-lifecycleandmcp-staleness-banner, the one test each that failed then passes when run alone.Kernel path (e0bbb66 plus this fix; the TS walker hasn't changed since). The kernel was built and loaded for TS/TSX. The new tests, a 6-scenario sync-vs-fresh matrix and the CLI repro above all converge.
Scenario matrix and CLI repro. Rerun on all three bases with wasm builds:
maindiverges in all 6 scenarios and the repro, and this branch converges in all of them.Full-index A/B (
init -y,scripts/dump-graph.mjs, on e0bbb66 and again on ed199e6):Late-module scenario. Every module a route reaches through a same-file lazy value was held out, the rest indexed, the files restored and
codegraph syncrun. Lines differing from a fresh index:mainmain, its seven routes (/,/user,/products,/blog,/sign-in,/404,/*) stay on theirUserPage-style declarations, while each module exportsPage.baseUrlpaths that codegraph doesn't resolve, so their routes bind to the declarations on a fresh index too.Generic late scenario (e0bbb66). Every 7th script file was held out, then the same restore and sync, on 11 repos. On every repo the fix's drift lines are a subset of
main's:/sign-inconverges.Cost.
sync-route-modulestakes 0–7 ms per sync on these repos. The query walks route edges: 4 ms on outline's 1,215, 0.1 ms on vscode's index.styledcomponents in.tsfiles) and 14 of vscode's 15,358. Even then it fires only when such a file gains or renames a component.Overlap
declaredComponent: more loader shapes, wrapped functions, a module that forwards its component, and resolution in the importing file's language first. It also applies the rule beyond route refs: every Pattern 1 ref from a.tsx/.jsxfile, layouts andjsx-render. Whichever lands second should:declaredLoaderand the module path throughlazyModuleFile, solazyModulesand resolution keep agreeing;lazyModules' route condition to Pattern 1's and includelayout:<Name>;jsx-renderreads the same module. This PR's delta trigger covers a module that gains a component, but not an edit that only moves its default export.lazyRouteComponentfollow barrels (export * from,export { default } from) for up to 4 hops. This PR landed second and makeslazyModulesname every file that walk reads, so the barrel test above converges.indexshould feed the same list when it lands.lazy-import:reference behind two barrels is fix(sync): a route links the module it lazily loads when the module appears later #2422's retry path, not this one. It is fix(react-router): a lazy route links the page a barrel module forwards #2436's open follow-up ("relink a page two barrels deep behind a lazy route"), which can reuselazyModules.jsx-renderreads as tags. It leftjsxChild's candidate kinds alone, soisJsxChildCandidatestill mirrors them, and its tests pass on the merge.module:tails. This PR re-opens resolved ones, so the two cover different rows. Its sync changes merged without conflict.navigationhook beside this one, plus trap 16; fix(react-router): a JSX index route is the page at its parent's address, inside the layout around it #2423 and fix(frameworks): a framework resolves only references written in its own languages #2415 added traps 14 and 15. Thetypes.tsand trap-list conflicts resolved as keep-both, so this PR's trap is 17.Not in this PR
./pages/DocsfromDocs/index.tsxto a newDocs.tsx). The route's edge points into the old, unchanged file, so it isn't a candidate.🤖 Generated with Claude Code