Files
codegraph/__tests__/jsx-render-work.test.ts
Colby Mchenry 0951105538 fix(react): a tag that passes type arguments renders its generic component (#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>
2026-10-10 05:56:32 +00:00

85 lines
3.4 KiB
TypeScript

/**
* The jsx-render pass reads each function's own lines for the component tags
* it renders. Every function of a minified bundle spans the bundle's one line,
* so it split, sliced and scanned the whole file once per function: 2,234
* times over go-ethereum's 980 KB `graphiql.min.js`, 25 of the 28 seconds its
* linking passes took on Node 22 (9 of 13 on Node 24). Functions that span
* the same lines render the same tags, so they share one scan. Counted, never
* timed.
*/
import { describe, it, expect, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
/** The source of the pass's tag pattern, to count its scans by. */
const JSX_TAG_SOURCE = String.raw`<([A-Z][A-Za-z0-9_]*)(?=[\s/><])`;
describe('jsx-render pass work', () => {
let tmpDir: string | undefined;
let cg: CodeGraph | undefined;
const originalExec = RegExp.prototype.exec;
afterEach(() => {
RegExp.prototype.exec = originalExec;
cg?.close();
cg = undefined;
if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5 });
tmpDir = undefined;
});
async function index(files: Record<string, string>): Promise<CodeGraph> {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-jsx-work-'));
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"dependencies":{"react":"^18.0.0"}}');
for (const [file, content] of Object.entries(files)) fs.writeFileSync(path.join(tmpDir, file), content);
cg = CodeGraph.initSync(tmpDir);
await cg.indexAll();
return cg;
}
/** What each named function renders, by jsx-render edge. */
const renders = (graph: CodeGraph, name: string): string[] => {
const from = graph.getNodesByName(name).find((n) => n.kind === 'function' || n.kind === 'component')!;
return graph.getOutgoingEdges(from.id)
.filter((e) => (e.metadata as { synthesizedBy?: string } | undefined)?.synthesizedBy === 'jsx-render')
.map((e) => graph.getNode(e.target)!.name);
};
it('scans a line that many components share once, not once per component', async () => {
const COMPONENTS = 60;
const bundle = Array.from({ length: COMPONENTS }, (_, i) => `function Card${i}(){return <Badge/>}`).join('');
// A scan of the bundle's line: the tag pattern run over it from the start.
let scans = 0;
RegExp.prototype.exec = function (this: RegExp, input: string) {
if (this.source === JSX_TAG_SOURCE && this.lastIndex === 0 && typeof input === 'string' &&
input.includes(`Card${COMPONENTS - 1}(`)) scans++;
return originalExec.call(this, input);
};
const graph = await index({
'bundle.jsx': `${bundle}\n`,
'badge.jsx': 'export function Badge(){return null}\n',
});
RegExp.prototype.exec = originalExec;
for (const i of [0, 31, COMPONENTS - 1]) expect(renders(graph, `Card${i}`)).toEqual(['Badge']);
expect(scans).toBeGreaterThan(0);
expect(scans).toBeLessThan(COMPONENTS / 4);
});
it('keeps the tags of functions on different lines apart', async () => {
const graph = await index({
'page.jsx': [
'export function Page() { return <Header/>; }',
'export function Footer() {',
' return <Links/>;',
'}',
'function Header(){return null} function Links(){return null}',
'',
].join('\n'),
});
expect(renders(graph, 'Page')).toEqual(['Header']);
expect(renders(graph, 'Footer')).toEqual(['Links']);
});
});