Skip to content
Open
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
41 changes: 41 additions & 0 deletions src/__tests__/exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
});
});

12 changes: 12 additions & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

78 changes: 73 additions & 5 deletions src/verify-exports.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -8,6 +9,7 @@ import {
AddressDisplay,
AssetPill,
type Balance,
BatchPaymentProcessor,
type ClaimableBalance,
type ContractEvent,
ContractEventFeed,
Expand All @@ -18,26 +20,91 @@ 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, string[]>,
): string[][] {
const visited = new Set<string>();
const inStack = new Set<string>();
const pathStack: string[] = [];
const cycles: string[][] = [];
const seenCycleKeys = new Set<string>();

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,
AddressDisplay,
AssetPill,
ContractEventFeed,
GovernanceDashboard,
});
PortfolioRebalancer,
SwapSimulator,
BatchPaymentProcessor,
Sidebar,
TopBar,
useFeeData,
useIsVisible,
useOutsideClick,
};

// Dummy type usage to prevent unused type warnings
export type TestExports = {
Expand All @@ -57,4 +124,5 @@ export type TestExports = {
governanceProposal: GovernanceProposal;
proposalStatus: ProposalStatus;
voteChoice: VoteChoice;
navSection: NavSection;
};