Repository navigation
fix(react): a JSX tag imported from the project renders what its module exports - #2563
Merged
Merged
Conversation
…le exports
The JSX render pass matched a tag by name and used the file's imports only
to break ties, so an index that forwards another module's component
declared no candidate and the tag took a namesake elsewhere: outline's
Sidebar/index.ts is `import Sidebar from "./App"; export default Sidebar`,
and <Sidebar> rendered Aside.tsx's styled Sidebar, not the AppSidebar that
App.tsx exports as observer(AppSidebar).
A tag imported from a module of the project now renders what that module
exports under the imported name, read by exportedComponent, the reader
lazy routes use: through an index or barrel, past a wrapper call. Two rules
join that reader, for routes too: a const holding another binding is that
binding (`const Avatar = AvatarWithHoverCard; export { Avatar }`; a let may
be reassigned, so it stays), and a memo wrapper's bodyless component node is
the component it wraps (`const MemoizedBlurhash = memo(Blurhash)`). A module
whose export is no component node (`export default () => ...`) links nothing
rather than a namesake. Sync records every file a tag's import is read
through as a synthesis input, so rewiring only the index redraws the tag.
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.
Summary
The JSX render pass (
jsxChildinsrc/resolution/callback-synthesizer.ts) matched a tag by name and used the file's imports only to break ties. So when a tag is imported through anindexthat forwards another module's component, the index declares no candidate and the name picks one elsewhere: outline'sSidebar/index.tsisimport Sidebar from "./App"; export default Sidebar, and<Sidebar>renderedAside.tsx's styledSidebarinstead of theAppSidebarthatApp.tsxexports asobserver(AppSidebar).A tag imported from a module of the project now renders what that module exports under the imported name. The export is read by
exportedComponentinframeworks/react.ts, the reader lazy routes already use (now exported), so the following are covered:indexor barrel:import X from './App'; export default X,export { X }of an import,export { default } from,export *(with JavaScript's ambiguity rule), and renamed default imports.observer(AppSidebar),React.memo(StaticCanvas, areEqual),React.forwardRef<…>(Button).constholding another binding is that binding. outline'sAvatar/index.tsisconst Avatar = AvatarWithHoverCard; export { Avatar }, and signoz hasexport const TanStackRowCell = TanStackRowCellMemo as typeof TanStackRowCellInner. If the binding leads nowhere, the value stays the export. Aletisn't followed, because it may be reassigned. ThewithAuth(Page)guard from fix(react-router): a lazy route follows a barrel that re-exports an import #2558 is unchanged.const MemoizedBlurhash = memo(Blurhash); export { MemoizedBlurhash as Blurhash }and excalidraw'sexport const Excalidraw = React.memo(ExcalidrawBase, areEqual)used to land on a component node with no body and no outgoing edges, so render flows dead-ended there.export default ({ url }) => …), the tag links nothing. Before, it linked a namesake elsewhere. A namesake the module itself declares still links; that covers a CommonJSmodule.exports = Card.codegraph syncand the watcher redraw the tag when only the forwarding index, or the module behind it, changes. The new sync tests compare against a fresh index.One existing expectation changed. In
react-router-lazy-barrel.test.ts, the/buildersroute's module isexport const Component = BuilderListPage, and the route now linksBuilderListPage(the function with the markup) instead of the alias constant, which has no edges.Scope
findExportedSymbol/defaultExportBindingNode). This PR doesn't touch the import resolver; it reuses the lazy-route reader inside the JSX pass.Validation
Fresh-index A/B. Main
d0bb9be5against this branch: fresh indexes of 13 repos, dumped withscripts/dump-graph.mjsand compared by natural keys. Every changed edge was graded by a TypeScript-checker oracle. The oracle binds the tag with the checker, follows import aliases, and follows a value that holds another binding to its declaration. A target is right if it is that declaration or the component its wrapper hands on. A tag whose name binds no component in the project is right with no edge.¹ The tag moved from a bodyless memo wrapper to the component it wraps:
<Excalidraw>→ExcalidrawBase, mastodon<ShortNumber>→ShortNumberRenderer.² The decline: outline's email
Header/Footer/Difftags, whose modules default-export an unnamed arrow and used to link the app's namesakes, and mastodon's 4connect()-wrapped containers.³ Includes one generic tag,
<TanStackTable<T, TItemKey>>, that the oracle can't locate. I checked it by hand.⁴ Includes two sites in
examples/with-nextjs, graded with that example's own tsconfig. The<Excalidraw>one is the same unwrap; the checker can't resolve the package import there.Tests:
jsx-child-disambiguation.test.tsand a lazy-route memo case inreact-router-lazy-barrel.test.ts. One tag case pins that aletalias is not followed, since it may be reassigned.sync-jsx-import-forwarding.test.ts: the index rewired (full and scoped sync), the index switching to a value alias, and the module behind the index rewrapped. Each case compares the synced graph with a fresh index.EBUSYdaemon teardowns, in the daemon, MCP freshness, sync and worktree suites. All 9 files pass when rerun serially (155/155).Timing: I compared main and this branch over 5 interleaved rounds per arm, on a shared box, and took medians.
jsxEdgespass wallA CPU profile of the pass puts the added time in comment-stripping the modules that tags import from (about 125 ms on signoz) and in resolving those imports' paths. I didn't add a cross-pass cache of module text: it would pin text the resolver's LRU file cache evicts on purpose.
🤖 Generated with Claude Code