'use client' import type React from 'react' import { useEffect, useMemo, useState } from 'react' import useSWR from 'swr' import { ErrorState } from '@/components/error-state' import { FileSearch } from '@/components/file-search' import { RouteTypeahead } from '@/components/route-typeahead' import { Sidebar } from '@/components/sidebar' import { TreemapVisualizer } from '@/components/treemap-visualizer' import { Badge } from '@/components/ui/badge' import { TreemapSkeleton } from '@/components/ui/skeleton' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { MultiSelect } from '@/components/ui/multi-select' import { AnalyzeData, ModulesData } from '@/lib/analyze-data' import { computeActiveEntries, computeModuleDepthMap } from '@/lib/module-graph' import { fetchStrict } from '@/lib/utils' import { formatBytes } from '@/lib/utils' import { SizeMode } from '@/lib/treemap-layout' import { Monitor, Server, FileCode, FileJson, Palette, Package, } from 'lucide-react' enum Environment { Client = 'client', Server = 'server', } export default function Home() { const [selectedRoute, setSelectedRoute] = useState(null) const [environmentFilter, setEnvironmentFilter] = useState( Environment.Client ) const [typeFilter, setTypeFilter] = useState(['js', 'css', 'json']) const [selectedSourceIndex, setSelectedSourceIndex] = useState( null ) const [focusedSourceIndex, setFocusedSourceIndex] = useState( null ) const { data: modulesData, isLoading: isModulesLoading, error: modulesError, } = useSWR('data/modules.data', fetchModulesData) let analyzeDataPath if (selectedRoute && selectedRoute === '/') { analyzeDataPath = 'data/analyze.data' } else if (selectedRoute) { analyzeDataPath = `data/${selectedRoute.replace(/^\//, '')}/analyze.data` } else { analyzeDataPath = null } const { data: analyzeData, isLoading: isAnalyzeLoading, error: analyzeError, } = useSWR(analyzeDataPath, fetchAnalyzeData, { revalidateOnFocus: false, revalidateOnReconnect: false, onSuccess: (newData) => { const newRootSourceIndex = getRootSourceIndex(newData) setSelectedSourceIndex(newRootSourceIndex) setFocusedSourceIndex(newRootSourceIndex) }, }) const [sidebarWidth, setSidebarWidth] = useState(20) // percentage const [isResizing, setIsResizing] = useState(false) const [isMouseInTreemap, setIsMouseInTreemap] = useState(false) const [hoveredNodeInfo, setHoveredNodeInfo] = useState<{ name: string size: number server?: boolean client?: boolean } | null>(null) const [searchQuery, setSearchQuery] = useState('') useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // esc clears current treemap source selection if (e.key === 'Escape') { const activeElement = document.activeElement const isInputFocused = activeElement && ['INPUT', 'TEXTAREA'].includes(activeElement.tagName) if (!isInputFocused) { e.preventDefault() const rootSourceIndex = getRootSourceIndex(analyzeData) setSelectedSourceIndex(rootSourceIndex) setFocusedSourceIndex(rootSourceIndex) } } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) }, [analyzeData]) // Compute module depth map from active entries const moduleDepthMap = useMemo(() => { if (!modulesData || !analyzeData) return new Map() const activeEntries = computeActiveEntries(modulesData, analyzeData) return computeModuleDepthMap(modulesData, activeEntries) }, [modulesData, analyzeData]) const filterSource = useMemo(() => { if (!analyzeData) return () => true return (sourceIndex: number) => { const flags = analyzeData.getSourceFlags(sourceIndex) // Check environment filter const hasEnvironment = (environmentFilter === Environment.Client && flags.client) || (environmentFilter === Environment.Server && flags.server) // Check type filter const hasType = (typeFilter.includes('js') && flags.js) || (typeFilter.includes('css') && flags.css) || (typeFilter.includes('json') && flags.json) || (typeFilter.includes('asset') && flags.asset) return hasEnvironment && hasType } }, [analyzeData, environmentFilter, typeFilter]) const handleMouseDown = () => { setIsResizing(true) } const handleMouseMove = (e: React.MouseEvent) => { if (!isResizing) return const newWidth = ((window.innerWidth - e.clientX) / window.innerWidth) * 100 setSidebarWidth(Math.max(10, Math.min(50, newWidth))) // Clamp between 10% and 50% } const handleMouseUp = () => { setIsResizing(false) } const error = analyzeError || modulesError const isAnyLoading = isAnalyzeLoading || isModulesLoading const rootSourceIndex = getRootSourceIndex(analyzeData) return (
{error && !analyzeData ? ( ) : isAnyLoading ? ( <>
{analyzeData && (
{hoveredNodeInfo ? ( <> {hoveredNodeInfo.name} {`${formatBytes(hoveredNodeInfo.size)} compressed`} {(hoveredNodeInfo.server || hoveredNodeInfo.client) && ( {hoveredNodeInfo.client && ( client )} {hoveredNodeInfo.server && ( server )} )} ) : ( 'Hover over a file to see details' )}
)}
) } const typeFilterOptions = [ { value: 'js', label: 'JavaScript', icon: , }, { value: 'css', label: 'CSS', icon: }, { value: 'json', label: 'JSON', icon: , }, { value: 'asset', label: 'Asset', icon: , }, ] function TopBar({ analyzeData, selectedRoute, setSelectedRoute, environmentFilter, setEnvironmentFilter, setSelectedSourceIndex, setFocusedSourceIndex, typeFilter, setTypeFilter, searchQuery, setSearchQuery, }: { analyzeData: AnalyzeData | undefined selectedRoute: string | null setSelectedRoute: (route: string | null) => void environmentFilter: Environment setEnvironmentFilter: (env: Environment) => void setSelectedSourceIndex: (index: number | null) => void setFocusedSourceIndex: (index: number | null) => void typeFilter: string[] setTypeFilter: (types: string[]) => void searchQuery: string setSearchQuery: (query: string) => void }) { return (
{ setSelectedRoute(route) setSelectedSourceIndex(null) setFocusedSourceIndex(null) }} />
{analyzeData && ( <> } triggerClassName="w-36" aria-label="Filter by file type" /> )}
) } function ControlDivider() { return } function getRootSourceIndex(analyzeData: AnalyzeData | undefined): number { if (!analyzeData) return 0 const sourceRoots = analyzeData.sourceRoots() return sourceRoots.length > 0 ? sourceRoots[0] : 0 } async function fetchAnalyzeData(url: string): Promise { const resp = await fetchStrict(url) return new AnalyzeData(await resp.arrayBuffer()) } async function fetchModulesData(url: string): Promise { const resp = await fetchStrict(url) return new ModulesData(await resp.arrayBuffer()) }