Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In TypeScript React code, a styled-components or emotion component declared with a type argument, like `const Wrapper = styled.div<WrapperProps>` followed by its CSS, or `styled(Base)<Props>`, is now indexed as a component, as one written without a type argument already was. Before, it was indexed as a plain constant, so `codegraph callers` and impact found none of the code that renders it, and its tags could link to another file's component of the same name instead: in outline/outline, `NudeButton` and `HStack` had no callers, and `<Text>` tags linked to a different `Text` than the one each file imports. Re-index TypeScript React projects after upgrading.
- `codegraph sync` and the file watcher now move a React Router route to its page when the router file loads that page lazily into a value of its own, like `const Docs = lazy(() => import('./pages/Docs'))` rendered by `<Route path="/docs" element={<Docs />} />`, and the page's file is added after the router was indexed, comes back after being deleted, or gets or changes its default export in a later edit. Before, the route kept pointing at the `Docs` declaration, or at the page's old component, until the router file itself changed or the project was re-indexed, unless the page's component happened to share the declaration's name. The same now holds for a `lazy: () => import('./pages/Docs')` route whose page switches its default export to another component. Re-index React projects after upgrading to correct routes an earlier version left behind.
- `codegraph sync` and the file watcher now link a component to the JSX that renders it, like `<Team />`, when the component is added in a file with no JSX of its own, as in `export default function Team() { return null; }`. Before, the link waited for an unrelated change to a file with JSX, or for a re-index, so the new component looked unused.
- In JavaScript and TypeScript, a default import now links to what the module's `export default` statement exports, wherever the statement sits in the file. Before, a module that declares its default, like `export default function Vans() {…}` or `export default class Store {…}`, had its default import linked to the first function it exports instead: in a React Router data-router app, where a page exports its `loader` or `action` above the page component, `import Vans, { loader as vansLoader } from './pages/Vans'` made the route `/vans` and every use of `Vans` lead to the loader. A component file that also exports a styled or memoized component no longer hands its default import to that component, and an anonymous default, like `export default function () {…}` or `export default () => …`, is no longer linked to a function exported beside it or nested inside it. Re-index JavaScript and TypeScript projects after upgrading.
- In JavaScript and TypeScript, a default import now links to what the module's `export default` statement exports, wherever the statement sits in the file. Before, a module that declares its default, like `export default function Vans() {…}` or `export default class Store {…}`, had its default import linked to the first function it exports instead: in a React Router data-router app, where a page exports its `loader` or `action` above the page component, `import Vans, { loader as vansLoader } from './pages/Vans'` made the route `/vans` and every use of `Vans` lead to the loader. A component file that also exports a styled or memoized component no longer hands its default import to that component, and an anonymous default, like `export default function () {…}` or `export default () => …`, is no longer linked to a function exported beside it or nested inside it. A module loaded with `require('./x').default` is read the same way. Re-index JavaScript and TypeScript projects after upgrading.
- In React and Vue, a tag for a component the file imports from a package, like `<Button>` from antd, `<Typography>` from `@mui/material/Typography`, `<Link>` from react-router-dom or `<Card>` from ant-design-vue, no longer links to a component, function or class in your repository that only shares its name. Before, the tag linked to whatever the repository had by that name, so `codegraph_explore`, callers and impact followed renders that never happen: in SigNoz, antd's `<Button>` tags showed up as callers of a styled link on its 404 page, and in Material UI dashboards like mantis and berry, `<Typography>` and `<Button>` tags showed up as callers of the theme's style overrides. A package here is one that a `package.json` above the file lists and that no file in the repository answers to. A tag imported through a path alias like `components/Modal`, from a workspace package of the repository, or under a name no `package.json` lists still links as before. Re-index React and Vue projects after upgrading.
- In React with TypeScript, a component that renders a generic component and passes it type arguments, like outline's `<PaginatedList<Document> items={…} />` or excalidraw's `<DropdownMenuItemContentRadio<"contain" | "overlap"> …>`, now links to the component it renders. Before, a tag followed by type arguments was skipped, so the generic component looked unused by the screens that render it, and `codegraph_explore`'s call paths, callers and impact stopped short of it. Re-index React projects after upgrading.

Expand Down
63 changes: 62 additions & 1 deletion __tests__/default-export-declaration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
* styled or memo component lost its default function or binding to that
* component, and an anonymous default (`export default function () {…}`,
* `export default () => …`) took whatever exported function came first — even
* one nested inside the default itself.
* one nested inside the default itself. `require('./x').default` of an ES
* module reads the same default, through the same lookup.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import * as fs from 'fs';
Expand Down Expand Up @@ -173,6 +174,37 @@ export default () => {
}

export default new Service()
`,
// An abstract class the statement declares, below an exported helper.
'src/repository.ts': `export function connect(): string {
return "db"
}

export default abstract class Repository {
abstract find(id: string): unknown

describe(): string {
return connect()
}
}
`,
// React Native's AnimatedColor shape, loaded with `require(…).default`
// (read the way a default import reads it) instead of imported.
'src/animated/AnimatedColor.ts': `export function getRgbaValueAndNativeColor(value: string): string {
return value
}

export default class AnimatedColor {
constructor(value: string) {
getRgbaValueAndNativeColor(value)
}
}
`,
'src/animated/load.ts': `export function makeColors(): unknown[] {
const AnimatedColor = require("./AnimatedColor").default
const { default: Color } = require("./AnimatedColor")
return [new AnimatedColor("red"), new Color("blue")]
}
`,
'src/use.tsx': `import handler from "./api/vans"
import Store from "./store"
Expand All @@ -183,6 +215,7 @@ import Banner from "./components/Banner"
import runAnonymous from "./anonymous"
import Arrow from "./arrow"
import service from "./service"
import Repository from "./repository"

export function consume() {
handler({}, { json() {} })
Expand All @@ -195,6 +228,12 @@ export function consume() {
Arrow()
service.run()
}

export class Users extends Repository {
find(id: string) {
return id
}
}
`,
};

Expand Down Expand Up @@ -253,6 +292,28 @@ describe('a default import of a module that exports something above its default'
expect(targets).not.toContain('src/stream.js:emit');
});

it('extends the abstract class the statement declares', () => {
const users = cg.getNodesInFile('src/use.tsx').find((n) => n.name === 'Users' && n.kind === 'class')!;
const bases = cg
.getOutgoingEdges(users.id)
.filter((e) => e.kind === 'extends')
.map((e) => `${cg.getNode(e.target)!.filePath}:${cg.getNode(e.target)!.name}`);
expect(bases).toEqual(['src/repository.ts:Repository']);
});

it('reads `require(…).default` and `{ default: X } = require(…)` the same way', () => {
const make = cg.getNodesInFile('src/animated/load.ts').find((n) => n.name === 'makeColors')!;
const created = cg
.getOutgoingEdges(make.id)
.filter((e) => e.kind === 'calls' || e.kind === 'instantiates')
.map((e) => `${e.kind} ${cg.getNode(e.target)!.filePath}:${cg.getNode(e.target)!.name}`)
.sort();
expect(created).toEqual([
'instantiates src/animated/AnimatedColor.ts:AnimatedColor',
'instantiates src/animated/AnimatedColor.ts:AnimatedColor',
]);
});

it('prefers what the statement exports to an exported component above it', () => {
const targets = consumed();
expect(targets).toContain('src/components/Title.tsx:Title');
Expand Down