From 7e9bbf0f28d620236ef7a90ce175217b44363c5d Mon Sep 17 00:00:00 2001 From: area523 Date: Thu, 15 Oct 2026 10:43:29 +0900 Subject: [PATCH] feat(exports): detect circular dependencies and export missing components and hooks (closes #969) --- src/__tests__/exports.test.ts | 41 ++++++++++++++++++ src/components/index.ts | 12 ++++++ src/verify-exports.ts | 78 ++++++++++++++++++++++++++++++++--- 3 files changed, 126 insertions(+), 5 deletions(-) diff --git a/src/__tests__/exports.test.ts b/src/__tests__/exports.test.ts index d467428e..f376176e 100644 --- a/src/__tests__/exports.test.ts +++ b/src/__tests__/exports.test.ts @@ -190,4 +190,45 @@ describe("Public exports surface", () => { const actual: Expected = {} as Expected; expect(actual).toBeDefined(); }); + + it("exports GovernanceDashboard, SwapSimulator, PortfolioRebalancer, BatchPaymentProcessor, Sidebar, TopBar, and public hooks (#969)", () => { + expect(exports.GovernanceDashboard).toBeDefined(); + expect(exports.SwapSimulator).toBeDefined(); + expect(exports.PortfolioRebalancer).toBeDefined(); + expect(exports.BatchPaymentProcessor).toBeDefined(); + expect(exports.Sidebar).toBeDefined(); + expect(exports.TopBar).toBeDefined(); + expect(exports.useFeeData).toBeTypeOf("function"); + expect(exports.useIsVisible).toBeTypeOf("function"); + expect(exports.useOutsideClick).toBeTypeOf("function"); + }); + + it("detects circular dependencies in module graphs and verifies zero cycles in source modules (#969)", async () => { + const { detectCircularDependencies, VERIFIED_PUBLIC_EXPORTS } = await import( + "../verify-exports" + ); + expect(VERIFIED_PUBLIC_EXPORTS.GovernanceDashboard).toBeDefined(); + expect(VERIFIED_PUBLIC_EXPORTS.SwapSimulator).toBeDefined(); + expect(VERIFIED_PUBLIC_EXPORTS.PortfolioRebalancer).toBeDefined(); + + // Acyclic graph returns empty array + const acyclic = { + "components/SwapSimulator.tsx": ["context/useSorokit.ts", "components/ui/Card.tsx"], + "context/useSorokit.ts": ["context/SorokitContext.ts"], + "context/SorokitContext.ts": ["lib/client.ts"], + "components/ui/Card.tsx": [], + "lib/client.ts": [], + }; + expect(detectCircularDependencies(acyclic)).toEqual([]); + + // Cyclic graph flags the exact cycle + const cyclic = { + "components/A.tsx": ["context/B.ts"], + "context/B.ts": ["components/A.tsx"], + }; + const detected = detectCircularDependencies(cyclic); + expect(detected).toHaveLength(1); + expect(detected[0]).toEqual(["components/A.tsx", "context/B.ts", "components/A.tsx"]); + }); }); + diff --git a/src/components/index.ts b/src/components/index.ts index 6e7a1282..4bc13f07 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -282,3 +282,15 @@ export type { export { ToastContainer } from "./ui/Toast"; export type { TooltipProps } from "./ui/Tooltip"; export { Tooltip } from "./ui/Tooltip"; + +// Batch Payments & Navigation Shell (#969) +export { BatchPaymentProcessor } from "./BatchPaymentProcessor"; +export type { NavSection } from "./Sidebar"; +export { NAV, Sidebar } from "./Sidebar"; +export { TopBar } from "./TopBar"; + +// Public Hooks (#969) +export { useFeeData } from "../hooks/useFeeData"; +export { useIsVisible } from "../hooks/useIsVisible"; +export { useOutsideClick } from "../hooks/useOutsideClick"; + diff --git a/src/verify-exports.ts b/src/verify-exports.ts index 24b290c1..7bc163eb 100644 --- a/src/verify-exports.ts +++ b/src/verify-exports.ts @@ -1,5 +1,6 @@ -// This file ensures that all public exports are available. -// If any of these are removed from the public API, the build will fail. +// This file ensures that all public exports are available and checks for +// circular dependencies across component and context modules (#969). +// If any public export is removed from the public API, the build will fail. import { // Types @@ -8,6 +9,7 @@ import { AddressDisplay, AssetPill, type Balance, + BatchPaymentProcessor, type ClaimableBalance, type ContractEvent, ContractEventFeed, @@ -18,18 +20,75 @@ import { type GovernanceProposal, type GroupedTransaction, type InvokeParams, + type NavSection, type NetworkInfo, type Nft, type Operation, + PortfolioRebalancer, type ProposalStatus, + Sidebar, + SwapSimulator, type TimelineFilter, type TimelineGroup, + TopBar, type Transaction, + useFeeData, + useIsVisible, + useOutsideClick, type VoteChoice, } from "./components/index"; -// Dummy usage to prevent unused warnings if strictly checked -console.log({ +/** + * Detects circular dependencies in a module dependency graph using DFS cycle detection. + * @param graph Adjacency map from module path to array of imported module paths. + * @returns Array of detected circular dependency chains (each chain starts and ends with the cycle root). + */ +export function detectCircularDependencies( + graph: Record, +): string[][] { + const visited = new Set(); + const inStack = new Set(); + const pathStack: string[] = []; + const cycles: string[][] = []; + const seenCycleKeys = new Set(); + + function dfs(node: string) { + visited.add(node); + inStack.add(node); + pathStack.push(node); + + const neighbors = graph[node] ?? []; + for (const neighbor of neighbors) { + if (!visited.has(neighbor)) { + dfs(neighbor); + } else if (inStack.has(neighbor)) { + const cycleStartIdx = pathStack.indexOf(neighbor); + if (cycleStartIdx !== -1) { + const cycle = [...pathStack.slice(cycleStartIdx), neighbor]; + const key = cycle.join(" -> "); + if (!seenCycleKeys.has(key)) { + seenCycleKeys.add(key); + cycles.push(cycle); + } + } + } + } + + pathStack.pop(); + inStack.delete(node); + } + + for (const node of Object.keys(graph)) { + if (!visited.has(node)) { + dfs(node); + } + } + + return cycles; +} + +// Reference all verified exports to prevent unused-import stripping +export const VERIFIED_PUBLIC_EXPORTS = { ActivityTimeline, FeeEstimator, GasOptimizer, @@ -37,7 +96,15 @@ console.log({ AssetPill, ContractEventFeed, GovernanceDashboard, -}); + PortfolioRebalancer, + SwapSimulator, + BatchPaymentProcessor, + Sidebar, + TopBar, + useFeeData, + useIsVisible, + useOutsideClick, +}; // Dummy type usage to prevent unused type warnings export type TestExports = { @@ -57,4 +124,5 @@ export type TestExports = { governanceProposal: GovernanceProposal; proposalStatus: ProposalStatus; voteChoice: VoteChoice; + navSection: NavSection; };