/** * A lazily loaded route module that is a barrel (luci-go's milo/ui): * `lazy: () => import('@/test_verdict/pages/invocation_page')` loads * `invocation_page/index.ts`, whose `export * from './invocation_page'` * forwards the `Component` that `invocation_page.tsx` declares. The route * renders that component; before, the barrel was read for an export of its * own, found none, and the route linked nothing. (milo's own folder is * `src/build/`; src-build-folders.test.ts covers routes into it.) */ import { describe, it, expect, afterAll, afterEach, beforeAll } from 'vitest'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { CodeGraph } from '../src'; let root = ''; let cg: CodeGraph; beforeAll(async () => { root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rr-lazy-barrel-')); const files: Record = { 'package.json': JSON.stringify({ name: 'ui', private: true, dependencies: { react: '^18', 'react-router': '^7' } }), 'tsconfig.json': JSON.stringify({ compilerOptions: { baseUrl: '.', paths: { '@/*': ['./src/*'] } } }), 'src/router.tsx': `import { lazy } from 'react'; import { createBrowserRouter } from 'react-router'; const Settings = lazy(() => import('@/settings')); export const router = createBrowserRouter([ { path: '/builds/:id', lazy: () => import('@/builds/build_page'), children: [{ path: 'overview', lazy: () => import('@/builds/build_page/overview_tab') }], }, { path: '/builders', lazy: () => import('@/builds/pages') }, { path: '/fleet', lazy: () => import('@/fleet/home_page') }, { path: '/login', lazy: () => import('@/auth/login_page') }, { path: '/graph', lazy: () => import('@/chronicle/graph_view') }, { path: '/media', lazy: () => import('@/media/media_page') }, { path: '/settings', element: }, { path: '/redirect', lazy: () => import('@/core/redirection_loader') }, { path: '/clash', lazy: () => import('@/clash') }, { path: '/loop', lazy: () => import('@/loop/a') }, { path: '/no-default', lazy: () => import('@/legacy') }, ]); `, // `export *` forwards the `Component` its module declares. 'src/builds/build_page/index.ts': `export * from './build_page'; `, 'src/builds/build_page/build_page.tsx': `export const BuildPage = () => null; export function Component() { return ; } `, 'src/builds/build_page/overview_tab/index.ts': `export * from './overview_tab'; `, 'src/builds/build_page/overview_tab/overview_tab.tsx': `export function Component() { return null; } `, // A barrel of barrels. 'src/builds/pages/index.ts': `export * from './builder_list_page'; `, 'src/builds/pages/builder_list_page/index.ts': `export * from './builder_list_page'; `, 'src/builds/pages/builder_list_page/builder_list_page.tsx': `export function BuilderListPage() { return null; } export const Component = BuilderListPage; `, // A named re-export, renamed to the name React Router reads. 'src/fleet/home_page/index.ts': `export { HomePage as Component } from './home_page'; `, 'src/fleet/home_page/home_page.tsx': `export function HomePage() { return null; } `, // The default export, forwarded. 'src/auth/login_page/index.ts': `export { default } from './login_page'; `, 'src/auth/login_page/login_page.tsx': `export default function LoginPage() { return null; } `, // An export clause in the module itself. 'src/chronicle/graph_view.tsx': `function GraphView() { return null; } export { GraphView as Component }; `, // A memoized export, whose wrapper has no body of its own to render from. 'src/media/media_page.tsx': `import { memo } from 'react'; function MediaPage() { return null; } const MemoizedMediaPage = memo(MediaPage); export { MemoizedMediaPage as Component }; `, // `React.lazy` reads the default export, through a barrel too. 'src/settings/index.ts': `export { default } from './settings_page'; `, 'src/settings/settings_page.tsx': `export default function SettingsPage() { return null; } `, // A module that only redirects renders nothing. 'src/core/redirection_loader.ts': `export function redirectionLoader() { return null; } export const loader = redirectionLoader; `, // Two modules forward a `Component` through `export *`: JavaScript exports neither. 'src/clash/index.ts': `export * from './first'; export * from './second'; `, 'src/clash/first.tsx': `export function Component() { return null; } `, 'src/clash/second.tsx': `export function Component() { return null; } `, // Barrels that forward each other. 'src/loop/a.ts': `export * from './b'; `, 'src/loop/b.ts': `export * from './a'; `, // `export *` forwards every name but the default. 'src/legacy/index.ts': `export * from './legacy_page'; `, 'src/legacy/legacy_page.tsx': `export default function LegacyPage() { return null; } `, }; for (const [rel, content] of Object.entries(files)) { fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); fs.writeFileSync(path.join(root, rel), content); } cg = await CodeGraph.init(root, { index: true }); }, 60_000); afterAll(() => { cg?.close(); if (root) fs.rmSync(root, { recursive: true, force: true }); }); /** What a route's `references` edges reach, as `name@file`: what it renders, or the layouts around it. */ const bindings = (routeName: string, layout: boolean): string[] => { const route = cg.getNodesByKind('route').find((r) => r.name === routeName); if (!route) return [`no route ${routeName}`]; return cg.getOutgoingEdges(route.id) .filter((e) => e.kind === 'references' && Boolean((e.metadata as Record | undefined)?.layout) === layout) .map((e) => cg.getNode(e.target)!) .map((n) => `${n.name}@${n.filePath}`); }; const renders = (routeName: string): string[] => bindings(routeName, false); describe('a lazy route module that is a barrel', () => { it('renders the Component an `export *` barrel forwards', () => { expect(renders('/builds/:id')).toEqual(['Component@src/builds/build_page/build_page.tsx']); expect(renders('/builds/:id/overview')).toEqual(['Component@src/builds/build_page/overview_tab/overview_tab.tsx']); }); it('follows a barrel that forwards another barrel', () => { // `export const Component = BuilderListPage` holds the page: the route renders it. expect(renders('/builders')).toEqual(['BuilderListPage@src/builds/pages/builder_list_page/builder_list_page.tsx']); }); it('renders inside the layout a barrel forwards', () => { expect(bindings('/builds/:id/overview', true)).toEqual(['Component@src/builds/build_page/build_page.tsx']); }); it('follows a named re-export to what it renames', () => { expect(renders('/fleet')).toEqual(['HomePage@src/fleet/home_page/home_page.tsx']); expect(renders('/login')).toEqual(['LoginPage@src/auth/login_page/login_page.tsx']); }); it('reads an export clause in the module itself', () => { expect(renders('/graph')).toEqual(['GraphView@src/chronicle/graph_view.tsx']); }); it('renders the component a memoized export wraps, not the wrapper', () => { // mastodon's `blurhash.tsx` exports `memo(Blurhash)` this way. expect(renders('/media')).toEqual(['MediaPage@src/media/media_page.tsx']); }); it('follows the barrel a React.lazy value loads', () => { expect(renders('/settings')).toEqual(['SettingsPage@src/settings/settings_page.tsx']); }); it('links nothing a barrel does not forward', () => { // A loader-only module, a name two `export *` modules both forward, a // cycle, and a default export behind `export *`. expect(renders('/redirect')).toEqual([]); expect(renders('/clash')).toEqual([]); expect(renders('/loop')).toEqual([]); expect(renders('/no-default')).toEqual([]); }); }); describe('sync, for a lazy route module that is a barrel', () => { type Files = Record; const ROUTER: Files = { 'package.json': JSON.stringify({ name: 'ui', private: true, dependencies: { react: '^18', 'react-router': '^7' } }), 'tsconfig.json': JSON.stringify({ compilerOptions: { baseUrl: '.', paths: { '@/*': ['./src/*'] } } }), 'src/router.tsx': `import { createBrowserRouter } from 'react-router'; export const router = createBrowserRouter([ { path: '/reports', lazy: () => import('@/reports/report_list') }, { path: '/reports/:id', lazy: () => import('@/reports/report_page') }, ]); `, 'src/reports/report_list/index.ts': `export { ReportList as Component } from './report_list'; `, 'src/reports/report_page/index.ts': `export * from './report_page'; `, }; const PAGES: Files = { 'src/reports/report_list/report_list.tsx': `export function ReportList() { return null; } `, 'src/reports/report_page/report_page.tsx': `export function Component() { return null; } `, }; const LINKED = { '/reports': ['ReportList@src/reports/report_list/report_list.tsx'], '/reports/:id': ['Component@src/reports/report_page/report_page.tsx'], }; let dirs: string[] = []; let graphs: CodeGraph[] = []; afterEach(() => { for (const graph of graphs) graph.close(); for (const dir of dirs) fs.rmSync(dir, { recursive: true, force: true }); graphs = []; dirs = []; }); const write = (dir: string, files: Files): void => { for (const [rel, content] of Object.entries(files)) { fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true }); fs.writeFileSync(path.join(dir, rel), content); } }; /** A project of `files`, indexed from scratch in a folder of its own. */ const indexed = async (files: Files): Promise<{ dir: string; graph: CodeGraph }> => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rr-lazy-barrel-sync-')); dirs.push(dir); write(dir, files); const graph = await CodeGraph.init(dir, { index: true }); graphs.push(graph); return { dir, graph }; }; /** Each route's `references` edges, as `name@file`. */ const routeLinks = (graph: CodeGraph): Record => Object.fromEntries( graph.getNodesByKind('route').map((route) => [route.name, graph.getOutgoingEdges(route.id) .filter((e) => e.kind === 'references') .map((e) => graph.getNode(e.target)!) .map((n) => `${n.name}@${n.filePath}`) .sort()]) ); it('links a page that appears behind its barrel later', async () => { const { dir, graph } = await indexed(ROUTER); expect(routeLinks(graph)).toEqual({ '/reports': [], '/reports/:id': [] }); write(dir, PAGES); expect((await graph.sync()).filesAdded).toBe(2); expect(routeLinks(graph)).toEqual(LINKED); expect(routeLinks((await indexed({ ...ROUTER, ...PAGES })).graph)).toEqual(LINKED); }, 60_000); it('links a page once an edit gives it the Component its barrel forwards', async () => { const { dir, graph } = await indexed({ ...ROUTER, ...PAGES, 'src/reports/report_page/report_page.tsx': 'export const placeholder = 1;\n' }); expect(routeLinks(graph)['/reports/:id']).toEqual([]); write(dir, { 'src/reports/report_page/report_page.tsx': PAGES['src/reports/report_page/report_page.tsx']! }); expect((await graph.sync()).filesModified).toBe(1); expect(routeLinks(graph)).toEqual(LINKED); }, 60_000); });