Skip to content

fix(react,vue): a tag imported from a package renders no project namesake - #2475

Merged
colbymchenry merged 2 commits into
mainfrom
claude/distracted-bhaskara-1be179
Oct 10, 2026
Merged

colbymchenry merged 2 commits into
mainfrom
claude/distracted-bhaskara-1be179

Conversation

@colbymchenry

@colbymchenry colbymchenry commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

Opened on behalf of the session that wrote this commit ("Stop package-imported JSX tags binding to namesakes"), which stopped after squashing it and before pushing it. The branch is pushed exactly as that session committed it (d3c2bbb, on main cd97bb3); the description below is its commit message. #2476 (generic tags, <PaginatedList<Document>>) is stacked on this branch and lands after it.

What

A JSX tag the file imports from a package (antd's <Button>, react-router's <Link>, @mui/material/Typography's default) renders that package's component, which the index does not hold. jsxChild took a project node of the same name anyway: a unique name without looking at the file's imports at all, and a repeated one by language and order, because a package import never resolves to a file. On SigNoz, antd and @signozhq/ui <Button>s rendered export const Button = styled(Link) from a 404 page's styles.

  • jsxChild: a component the file declares itself still comes first. Then a tag the file imports from a package binds nothing: the import's specifier passes ctx.isOutOfRepoImport, the test the name matcher applies to calls and values (no project file answers it, and a package.json on the file's way to the root declares it). An alias that reads like a package (components/Modal), an undeclared bare specifier and a workspace package keep the old rules.
  • Vue templates (vueTemplateEdges's resolve): the same rule for a PascalCase or kebab-case tag, a handler, and the composable a destructured handler comes from. A package-imported tag no longer falls back to a Nuxt auto-imported component either.

Tests

__tests__/jsx-package-imports.test.ts, 5 cases:

  • a tag imported from a declared package renders nothing, whether one or several project symbols share its name;
  • the same for a default import from a package subpath, and for a package import under another name;
  • a tag imported through an alias that reads like a package, or through a bare specifier no package.json declares, keeps the old rules;
  • a tag imported from a workspace package of the repository keeps them too;
  • a Vue template binds nothing for a tag or a composable imported from a package.

Validation

Fresh before/after indexes of 35 repos on main b635dd4: 6,550 jsx-render edges removed, and one added: a same-file component (refine's win95 AddButton) that the 30-children cap had crowded out. A script that reads each parent's import and the package.json chain, independent of the resolver, confirms every removed edge is a tag imported from a declared package. Nodes, refs, files and every other edge are unchanged, and 13 React and 5 Vue repos are byte-identical.

Checked again while stacking #2476 on this commit: npx tsc --noEmit is clean, and this branch plus #2476 passes 554/554 tests across 28 JSX, React, Vue, router, dead-code and framework files, including this PR's jsx-package-imports.test.ts.

Merged with main, and re-run there

Main had gained #2428, #2452, #2439, #2432 and #2440 since this commit's base (cd97bb3). Before the merge, origin/main (c0f45c7) was merged into this branch (711e436). Only CHANGELOG.md conflicted; both sides' bullets were kept, main's first. The gate's source and test patch is unchanged by the merge (same git patch-id), and squash 3418399 is tree-identical to 711e436.

Re-run on that tree against main c0f45c7, fresh indexes of 10 repos:

repo jsx-render edges removed / added
refine examples −4,130 / +1
SigNoz −1,005
mantis −157
outline −33
bulletproof-react −11
mastodon −4
excalidraw, create-t3-turbo, next-saas-starter, proshop_mern unchanged

No node, ref, file or other edge changed. Where both runs measured the same repo, the counts equal this PR's original A/B on b635dd4. A second, independent check (the TypeScript checker with module resolution and each app's tsconfig paths, following re-exports) classifies all 1,210 removals in SigNoz, mantis, outline, bulletproof-react and mastodon as tags imported from a package. On the merged tree, npx tsc --noEmit is clean and 639/639 tests pass across 32 files, including the tests of the main commits merged in.

Per-repo results of the original A/B (35 repos, main b635dd4)

Added after the merge by the session that wrote the commit. Fresh codegraph init -y indexes on both arms, with the same native kernel, compared with scripts/dump-graph.mjs. Each count is jsx-render edges removed / added. A script that reads the parent file's import and the package.json chain graded every removal as a tag imported from a declared package.

repo removed / added
refine examples −4,130 / +1
SigNoz −1,005
cord-field −313
berry −232
topcoder community-app −207
happa −189
vue-vben-admin (Vue) −164
mantis −157
chickadee −44
outline −33
binner −21
matx −19
bulletproof-react −11
bigbluebutton −11
react.dev −9
mastodon −4
takenote −1
byte-identical excalidraw, minimal-kit, next-saas-starter, create-t3-turbo, proshop_mern, vanlife, react-boilerplate, redux-realworld, jira-clone, react-hn, dho-web, windscribe, sensors-africa; Vue: vue3-element-admin, vue-element-admin, vue-realworld, gin-vue-admin, nuxt-movies

Notes on these results:

  • Packages behind most removals:
    • antd and @refinedev/* in refine;
    • antd and @signozhq/ui in SigNoz;
    • @mui/material in cord-field, berry, mantis, chickadee and matx;
    • grommet in happa;
    • topcoder-react-ui-kit and react-router-dom in topcoder;
    • antdv-next and reka-ui in vben.
  • Catch-all aliases: cord-field, topcoder and happa have a root "*" path alias, so they change only on a main that has fix(js,ts): a catch-all or node_modules path alias no longer makes package imports look local #2437.
  • The one added edge: in refine's win95 example, VideoClubPageCreateTitle has 33 tags, and main stopped at the 30-children cap before the file's own AddButton. react95's <Separator> no longer takes a slot, so AddButton now fits.
  • Kept on purpose:
    • create-t3-turbo's 26 @acme/ui tags, which come from a workspace package.
    • windscribe's 12 tags from ui. That is the repo's own sibling package (modules/ui): modules/ext/package.json lists it as a dependency, but the resolver maps it into the repo.
  • Full test suite: a full parallel run of the commit, on a machine at 100% CPU, had 30 failures in 30 files. All were hook or test timeouts, EBUSY on teardown, or a timing budget, and none were in the JSX, React, Vue or resolution suites. They were not re-run one by one. The 639/639 run above covers the suites this change touches.

Not in this PR

  • Imports of project modules: renamed defaults (SigNoz's import Modal from 'components/Modal', whose default export is CustomModal), barrels and forwarding indexes. Those are fix(react): a JSX tag renders what the file imports under that name #2451's.
    • fix(react): a JSX tag renders what the file imports under that name #2451's description records how it takes this gate over when it lands second:
      • importedJsxChild's if (!module) return undefined; becomes return ctx.isOutOfRepoImport?.(mapping.source, file, language) ? null : undefined;;
      • the standalone check in jsxChild goes;
      • importedFromPackage stays, for the Vue resolver.
  • Function values (matchFunctionRef), such as outline's Link from react-router-dom passed as a value. Another session is changing that in name-matcher.ts.
  • A Vue handler destructured without renaming: with const { toggle } = useFullscreen() and @click="toggle", the handler is still matched to a project toggle by name. That needs a local-binding rule, like fix(react): a type argument or a name the component binds itself renders no JSX child #2442's for JSX, not an import rule.
  • A component nested inside another function: it still beats a package import of the same name used in a sibling function, because scope isn't modeled. None of the 35 repos has this shape.

🤖 Generated with Claude Code

…sake

A JSX tag the file imports from a package (antd's `<Button>`, react-router's
`<Link>`, `@mui/material/Typography`'s default) renders that package's
component, which the index does not hold. `jsxChild` took a project node of
the same name anyway: a unique name without looking at the file's imports at
all, and a repeated one by language and order, because a package import never
resolves to a file. On SigNoz, antd and @signozhq/ui `<Button>`s rendered
`export const Button = styled(Link)` from a 404 page's styles.

- `jsxChild`: a component the file declares itself still comes first. Then a
  tag the file imports from a package binds nothing: the import's specifier
  passes `ctx.isOutOfRepoImport`, the test the name matcher applies to calls
  and values (no project file answers it, and a package.json on the file's
  way to the root declares it). An alias that reads like a package
  (`components/Modal`), an undeclared bare specifier and a workspace package
  keep the old rules.
- Vue templates (`vueTemplateEdges`'s `resolve`): the same rule for a
  PascalCase or kebab-case tag, a handler, and the composable a destructured
  handler comes from. A package-imported tag no longer falls back to a Nuxt
  auto-imported component either.

Fresh before/after indexes of 35 repos on main b635dd4: 6,550 jsx-render
edges removed, and one added: a same-file component (refine's win95
`AddButton`) that the 30-children cap had crowded out. A script that reads
each parent's import and the package.json chain, independent of the
resolver, confirms every removed edge is a tag imported from a declared
package. Nodes, refs, files and every other edge are unchanged, and 13 React
and 5 Vue repos are byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit 3418399 into main Oct 10, 2026
colbymchenry added a commit that referenced this pull request Oct 10, 2026
…onent (#2476)

The jsx-render pass read a tag's name only where whitespace, `/` or `>`
ended it, so a generic component's tag, whose type arguments follow its
name, was never read: outline's `<PaginatedList<Document> items={…} />`
and excalidraw's `<DropdownMenuItemContentRadio<"contain" | "overlap">`
left the parent with no edge to the component it renders. The tag pattern
now also ends a name at `<`, as a lookahead, so that `<` still opens the
next match, which `opensTag` (#2442) reads as a type argument, not a tag.

This lands after the package-import gate for JSX tags (#2475). On main alone the
same change also linked 5 package tags to project namesakes by name
(refine's react95 `<Select<number>>`, antd `<Table<IPost>>` and
`<Form<ILoginForm>>`; signoz's antd `<Form<…>>` ×2 to a styled `Form`),
which the gate declines.

Fresh before/after indexes with the gate as the base: 22 jsx-render edges
added and nothing else changed (outline 10 to `PaginatedList`, signoz 8,
excalidraw 2, mastodon 2). Each target is what the file's import or own
declaration binds, checked with the TypeScript checker with module
resolution. refine-examples, bulletproof-react, mantis, proshop_mern,
next-saas-starter and create-t3-turbo are byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry deleted the claude/distracted-bhaskara-1be179 branch October 10, 2026 06:15
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.

2 participants