; });
export const Boxed = styled.div\`color: red;\`;
export const Wrapped = styled(Button)\`padding: 4px;\`;
export const Rewrapped = memo(Button);
`
);
const db = await index();
for (const name of ['Button', 'Bare', 'Card', 'Named', 'Boxed', 'Wrapped', 'Rewrapped']) {
expect(kindsOf(db, name), `${name} should be a component`).toContain('component');
// The bug was that these stayed plain constants.
expect(kindsOf(db, name), `${name} should not remain a constant`).not.toContain('constant');
}
});
it('classifies typed styled consts as component nodes too', async () => {
// tree-sitter's tagged-template call takes no type arguments, so a type
// argument that also reads as an expression turns the initializer into
// comparisons: `(styled.div < WrapperProps) > \`…\``. These shapes come from
// outline/outline (FloatingToolbar's Wrapper and Background, Lightbox's
// CloseAction, NudeButton, HStack).
fs.writeFileSync(
path.join(dir, 'typed.tsx'),
`import styled from 'styled-components';
import { s, depths } from './theme';
type WrapperProps = { active: boolean };
type Props = { align: 'start' | 'end' };
const Plain = styled.div\`color: red;\`;
export const Wrapper = styled.div\`
z-index: \${depths.editorToolbar};
color: \${(props) => (props.active ? s("accent") : s("text"))};
\`;
const Background = styled.div<{ align: Props["align"] }>\`color: red;\`;
const CloseAction = styled.div<{ animation: Animation | null }>\`top: 0;\`;
const Content = styled(Plain)\`padding: 4px;\`;
const NudeButton = styled(Plain).attrs((props: Props) => ({ type: "button" }))\`width: 24px;\`;
export const HStack = styled(Plain)
.withConfig({ shouldForwardProp: (prop) => prop !== "spacing" })
.attrs((props) => ({ align: props.align }))\`\`;
const Either = styled.div\`color: red;\`;
const Both = styled.span\`color: red;\`;
const Nested = styled.div>\`color: red;\`;
const Deeper = styled.div>>\`color: red;\`;
const Named = styled("figure")\`margin: 0;\`;
`
);
const db = await index();
for (const name of [
'Wrapper', 'Background', 'CloseAction', 'Content', 'NudeButton', 'HStack',
'Either', 'Both', 'Nested', 'Deeper', 'Named',
]) {
expect(kindsOf(db, name), `${name} should be a component`).toEqual(['component']);
}
});
it('gives a typed styled const its jsx-render callers', async () => {
fs.writeFileSync(
path.join(dir, 'toolbar.tsx'),
`import styled from 'styled-components';
type WrapperProps = { active: boolean };
export const Wrapper = styled.div\`opacity: 0;\`;
export function FloatingToolbar() {
return menu;
}
`
);
fs.writeFileSync(
path.join(dir, 'page.tsx'),
`import { Wrapper } from './toolbar';
export function Page() {
return ;
}
`
);
const db = await index();
const callers = db
.prepare(
`SELECT s.name caller FROM edges e
JOIN nodes s ON s.id = e.source
JOIN nodes t ON t.id = e.target
WHERE json_extract(e.metadata, '$.synthesizedBy') = 'jsx-render'
AND t.kind = 'component' AND t.name = 'Wrapper'
ORDER BY s.name`
)
.all()
.map((r: any) => r.caller);
expect(callers).toEqual(['FloatingToolbar', 'Page']);
});
it('keeps other typed template tags and real comparisons constants (precision)', async () => {
fs.writeFileSync(
path.join(dir, 'tags.tsx'),
`import styled, { css, createGlobalStyle, keyframes } from 'styled-components';
type Props = { open: boolean };
export const Mixin = css\`color: red;\`;
export const GlobalStyle = createGlobalStyle\`body { margin: 0; }\`;
export const Fade = keyframes\`from { opacity: 0; }\`;
export const Styledish = styledLike\`color: red;\`;
export const Compared = styled.length < LIMIT > 2;
export const Untagged = styled.div < Props > other;
const lowerCase = styled.div\`color: red;\`;
`
);
const db = await index();
for (const name of ['Mixin', 'GlobalStyle', 'Fade', 'Styledish', 'Compared', 'Untagged', 'lowerCase']) {
expect(kindsOf(db, name), `${name} must stay a constant`).toEqual(['constant']);
}
});
it('emits jsx-render edges so getCallers/getImpactRadius resolve a forwardRef component', async () => {
fs.writeFileSync(
path.join(dir, 'button.tsx'),
`import * as React from 'react';
export const Button = React.forwardRef((props, ref) => );
`
);
fs.writeFileSync(
path.join(dir, 'page.tsx'),
`import { Button } from './button';
export function Page() {
return ;
}
`
);
const db = await index();
// The render edge exists and is the synthesized jsx-render kind.
const edgeRows = db
.prepare(
`SELECT s.name caller FROM edges e
JOIN nodes s ON s.id = e.source
JOIN nodes t ON t.id = e.target
WHERE json_extract(e.metadata, '$.synthesizedBy') = 'jsx-render'
AND t.kind = 'component' AND t.name = 'Button'`
)
.all();
expect(edgeRows.map((r: any) => r.caller)).toContain('Page');
// ...and it surfaces through the public callers API (the issue's symptom:
// "No callers found" before the fix).
const buttonId = db
.prepare("SELECT id FROM nodes WHERE name='Button' AND kind='component'")
.get().id as string;
const callers = cg.getCallers(buttonId).map((c: any) => c.node.name);
expect(callers).toContain('Page');
});
it('captures the inner render-fn body callees under the component', async () => {
fs.writeFileSync(
path.join(dir, 'widget.tsx'),
`import * as React from 'react';
function useThing() { return 1; }
export const Widget = React.forwardRef((props, ref) => {
const v = useThing();
return
{v}
;
});
`
);
const db = await index();
const rows = db
.prepare(
`SELECT t.name FROM edges e
JOIN nodes s ON s.id = e.source
JOIN nodes t ON t.id = e.target
WHERE s.name = 'Widget' AND s.kind = 'component'
AND e.kind = 'calls' AND t.name = 'useThing'`
)
.all();
expect(rows.length).toBeGreaterThanOrEqual(1);
});
it('does not misclassify non-component PascalCase consts (precision)', async () => {
fs.writeFileSync(
path.join(dir, 'controls.tsx'),
`import * as React from 'react';
const cache = memo(expensiveFn);
export const Config = loadConfig();
export const Client = new ApiClient();
export const Styles = styledHelper();
export const Total = [1, 2].reduce((a, b) => a + b, 0);
export const Theme = { color: 'red' };
`
);
const db = await index();
for (const name of ['Config', 'Client', 'Styles', 'Total', 'Theme']) {
expect(kindsOf(db, name), `${name} must stay a constant`).toContain('constant');
expect(kindsOf(db, name), `${name} must not be a component`).not.toContain('component');
}
// A lowercase-named memo() result is a memoization util, not a component.
expect(kindsOf(db, 'cache')).not.toContain('component');
});
});