/**
* A data router's nested, lazy, constant-named routes (bulletproof-react):
* - a child's `path` is relative to its parent's — `discussions` under `/app`
* is `/app/discussions`;
* - `lazy: () => import('./routes/x')` renders the module's default export;
* - `path: paths.app.root.path` reads the constant object where it is
* declared — here through a monorepo app's own tsconfig `@/*` alias, which
* now resolves from the tsconfig nearest the importing file;
* - `element: ()` renders AppRoot.
*/
import { describe, it, expect, afterAll, 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-nested-'));
const app = (rel: string) => `apps/web/${rel}`;
const files: Record = {
'package.json': JSON.stringify({ name: 'mono', private: true }),
[app('package.json')]: JSON.stringify({ name: 'web', dependencies: { react: '^18', 'react-router': '^7' } }),
[app('tsconfig.json')]: JSON.stringify({ compilerOptions: { baseUrl: '.', paths: { '@/*': ['./src/*'] } } }),
[app('src/config/paths.ts')]: `export const paths = {
home: { path: '/', getHref: () => '/' },
app: {
root: { path: '/app', getHref: () => '/app' },
discussions: { path: 'discussions', getHref: () => '/app/discussions' },
},
} as const;
`,
[app('src/app/router.tsx')]: `import { createBrowserRouter } from 'react-router';
import { paths } from '@/config/paths';
import { ProtectedRoute } from '@/lib/auth';
import AppRoot from './routes/app/root';
export const router = createBrowserRouter([
{ path: paths.home.path, lazy: () => import('./routes/landing') },
{
path: paths.app.root.path,
element: (
),
children: [
{ path: paths.app.discussions.path, lazy: () => import('./routes/app/discussions') },
{ path: 'settings', element: },
],
},
]);
`,
[app('src/lib/auth.tsx')]: `export function ProtectedRoute({ children }: { children: unknown }) { return children; }
`,
[app('src/app/routes/landing.tsx')]: `const LandingRoute = () => null;
export default LandingRoute;
`,
[app('src/app/routes/app/root.tsx')]: `export default function AppRoot() { return null; }
`,
[app('src/app/routes/app/discussions.tsx')]: `export default function DiscussionsRoute() { 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 });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
const bindings = (routeName: string, layout: boolean): string[] => {
const route = cg.getNodesByKind('route').find((r) => r.name === routeName);
if (!route) return [];
return cg.getOutgoingEdges(route.id)
.filter((e) => e.kind === 'references' && Boolean((e.metadata as Record | undefined)?.layout) === layout)
.map((e) => cg.getNode(e.target)!.name);
};
const renders = (routeName: string): string[] => bindings(routeName, false);
describe('nested, lazy and constant React Router routes', () => {
it('compose their paths and render their modules', () => {
const names = cg.getNodesByKind('route').map((r) => r.name).sort();
expect(names).toEqual(['/', '/app', '/app/discussions', '/app/settings']);
expect(renders('/')).toEqual(['LandingRoute']);
expect(renders('/app')).toEqual(['AppRoot']);
expect(renders('/app/discussions')).toEqual(['DiscussionsRoute']);
});
it('a child route renders inside its parent’s component, the layout around it', () => {
expect(bindings('/app/discussions', true)).toEqual(['AppRoot']);
expect(bindings('/', true)).toEqual([]);
});
it('resolves an app’s own tsconfig alias', () => {
const router = cg.getNodesInFile('apps/web/src/app/router.tsx').map((n) => n.id);
const imported = cg.getOutgoingEdgesFrom(router).filter((e) => e.kind === 'imports').map((e) => cg.getNode(e.target)!.filePath);
expect(imported).toContain('apps/web/src/config/paths.ts');
expect(imported).toContain('apps/web/src/lib/auth.tsx');
});
});