mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 05:41:18 +08:00
0951105538
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>
85 lines
3.4 KiB
TypeScript
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']);
|
|
});
|
|
});
|