Initial Commit

* Implements Select package.json and parse dependencies
* Implements Bun API to handle package Analysis
* Implements NPM Registry API handlers
* Analyze the cross dependencies between packages
* Implements loading NodeJS versions with search (inclusing LTS)
* Error handling
* Implements Report option to get higher infor about the dependencies
* Implements Upgrade command builder
This commit is contained in:
2026-04-14 22:27:06 +05:30
commit 0e561a3fc8
32 changed files with 3285 additions and 0 deletions

64
src/APITester.tsx Normal file
View File

@@ -0,0 +1,64 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { useRef, type FormEvent } from "react";
export function APITester() {
const responseInputRef = useRef<HTMLTextAreaElement>(null);
const testEndpoint = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
const form = e.currentTarget;
const formData = new FormData(form);
const endpoint = formData.get("endpoint") as string;
const url = new URL(endpoint, location.href);
const method = formData.get("method") as string;
const res = await fetch(url, { method });
const data = await res.json();
responseInputRef.current!.value = JSON.stringify(data, null, 2);
} catch (error) {
responseInputRef.current!.value = String(error);
}
};
return (
<div className="flex flex-col gap-6">
<form onSubmit={testEndpoint} className="flex items-center gap-2">
<Label htmlFor="method" className="sr-only">
Method
</Label>
<Select name="method" defaultValue="GET">
<SelectTrigger className="w-[100px]" id="method">
<SelectValue placeholder="Method" />
</SelectTrigger>
<SelectContent align="start">
<SelectItem value="GET">GET</SelectItem>
<SelectItem value="PUT">PUT</SelectItem>
</SelectContent>
</Select>
<Label htmlFor="endpoint" className="sr-only">
Endpoint
</Label>
<Input id="endpoint" type="text" name="endpoint" defaultValue="/api/hello" placeholder="/api/hello" />
<Button type="submit" variant="secondary">
Send
</Button>
</form>
<Label htmlFor="response" className="sr-only">
Response
</Label>
<Textarea
ref={responseInputRef}
id="response"
readOnly
placeholder="Response will appear here..."
className="min-h-[140px] font-mono resize-y"
/>
</div>
);
}

26
src/App.tsx Normal file
View File

@@ -0,0 +1,26 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { APITester } from "./APITester";
import "./index.css";
import "@radix-ui/themes/styles.css";
import logo from "./logo.svg";
import reactLogo from "./react.svg";
import { PackageDetails } from "./PackageDetails";
import { Toaster } from "sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { Theme } from "@radix-ui/themes";
export function App() {
return (
<Theme appearance="dark">
<TooltipProvider>
<Toaster position="top-center" richColors theme="dark" />
<div className="container mx-auto p-8 relative z-10">
<PackageDetails />
</div>
</TooltipProvider>
</Theme>
);
}
export default App;

View File

@@ -0,0 +1,171 @@
import * as Dialog from "@radix-ui/react-dialog";
import { Button } from "./components/ui/button";
import { X, CheckCircle2, AlertCircle, XCircle } from "lucide-react";
import "./PackageSelector.css";
interface AnalysisResult {
name: string;
currentVersion: string;
latestVersion?: string;
type?: 'dependencies' | 'devDependencies';
engines?: {
node?: string;
npm?: string;
};
peerDependencies?: Record<string, string>;
nodeCompatible?: boolean;
peerConflicts?: Array<{ package: string; required: string; installed?: string }>;
}
interface CompatibilityOverviewProps {
open: boolean;
onClose: () => void;
results: AnalysisResult[];
nodeVersion: string;
}
export function CompatibilityOverview({ open, onClose, results, nodeVersion }: CompatibilityOverviewProps) {
const incompatiblePackages = results.filter(r => !r.nodeCompatible);
const peerConflictPackages = results.filter(r => r.peerConflicts && r.peerConflicts.length > 0);
const compatiblePackages = results.filter(r => r.nodeCompatible && (!r.peerConflicts || r.peerConflicts.length === 0));
return (
<Dialog.Root open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
<Dialog.Portal>
<Dialog.Overlay className="DialogOverlay" />
<Dialog.Content className="DialogContent" style={{ maxWidth: '900px', maxHeight: '90vh' }}>
<div className="flex items-start justify-between mb-4">
<div>
<Dialog.Title className="text-xl font-semibold">Compatibility Report</Dialog.Title>
<Dialog.Description className="text-sm text-muted-foreground mt-1">
Analyzing {results.length} packages for Node.js v{nodeVersion}
</Dialog.Description>
</div>
<Dialog.Close asChild>
<button className="rounded-md p-1.5 hover:bg-accent transition-colors" aria-label="Close">
<X className="h-4 w-4" />
</button>
</Dialog.Close>
</div>
<div className="space-y-4 overflow-y-auto" style={{ maxHeight: 'calc(90vh - 180px)' }}>
{/* Summary */}
<div className="grid grid-cols-3 gap-4">
<div className="p-4 bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-900 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<CheckCircle2 className="h-5 w-5 text-green-600 dark:text-green-500" />
<div className="text-sm font-medium text-green-900 dark:text-green-100">Compatible</div>
</div>
<div className="text-3xl font-bold text-green-700 dark:text-green-400">{compatiblePackages.length}</div>
<div className="text-xs text-green-600 dark:text-green-500 mt-1">No issues found</div>
</div>
<div className="p-4 bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-900 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<AlertCircle className="h-5 w-5 text-amber-600 dark:text-amber-500" />
<div className="text-sm font-medium text-amber-900 dark:text-amber-100">Node Issues</div>
</div>
<div className="text-3xl font-bold text-amber-700 dark:text-amber-400">{incompatiblePackages.length}</div>
<div className="text-xs text-amber-600 dark:text-amber-500 mt-1">Version conflicts</div>
</div>
<div className="p-4 bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-900 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<XCircle className="h-5 w-5 text-red-600 dark:text-red-500" />
<div className="text-sm font-medium text-red-900 dark:text-red-100">Peer Conflicts</div>
</div>
<div className="text-3xl font-bold text-red-700 dark:text-red-400">{peerConflictPackages.length}</div>
<div className="text-xs text-red-600 dark:text-red-500 mt-1">Dependency issues</div>
</div>
</div>
{/* Node.js Incompatible Packages */}
{incompatiblePackages.length > 0 && (
<div className="space-y-2">
<h3 className="font-semibold text-sm flex items-center gap-2 text-amber-900 dark:text-amber-100">
<AlertCircle className="h-4 w-4" />
Node.js Compatibility Issues
</h3>
<div className="space-y-2">
{incompatiblePackages.map(pkg => (
<div key={pkg.name} className="p-3 bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-900 rounded-lg">
<div className="flex justify-between items-start gap-3">
<div className="flex-1">
<div className="font-mono font-semibold text-sm">{pkg.name}</div>
<div className="text-xs text-muted-foreground mt-1">
Version: {pkg.latestVersion || pkg.currentVersion}
</div>
</div>
<div className="text-xs text-right">
<div className="text-muted-foreground">Requires</div>
<div className="font-mono text-amber-700 dark:text-amber-400 font-medium">{pkg.engines?.node || 'N/A'}</div>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Peer Dependency Conflicts */}
{peerConflictPackages.length > 0 && (
<div className="space-y-2">
<h3 className="font-semibold text-sm flex items-center gap-2 text-red-900 dark:text-red-100">
<XCircle className="h-4 w-4" />
Peer Dependency Conflicts
</h3>
<div className="space-y-2">
{peerConflictPackages.map(pkg => (
<div key={pkg.name} className="p-3 bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-900 rounded-lg">
<div className="font-mono font-semibold text-sm mb-2">{pkg.name}</div>
<div className="space-y-1.5">
{pkg.peerConflicts?.map((conflict, idx) => (
<div key={idx} className="text-xs pl-3 border-l-2 border-red-300 dark:border-red-800">
<span className="font-mono text-red-700 dark:text-red-400">{conflict.package}</span>
<span className="text-muted-foreground"> requires </span>
<span className="font-mono font-medium">{conflict.required}</span>
{conflict.installed && (
<>
<span className="text-muted-foreground"> (installed: </span>
<span className="font-mono text-amber-700 dark:text-amber-400">{conflict.installed}</span>
<span className="text-muted-foreground">)</span>
</>
)}
</div>
))}
</div>
</div>
))}
</div>
</div>
)}
{/* Compatible Packages */}
{compatiblePackages.length > 0 && (
<div className="space-y-2">
<h3 className="font-semibold text-sm flex items-center gap-2 text-green-900 dark:text-green-100">
<CheckCircle2 className="h-4 w-4" />
Compatible Packages
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{compatiblePackages.map(pkg => (
<div key={pkg.name} className="p-2.5 bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-900 rounded-md">
<div className="font-mono text-xs truncate">{pkg.name}</div>
<div className="text-xs text-muted-foreground">
v{pkg.latestVersion || pkg.currentVersion}
</div>
</div>
))}
</div>
</div>
)}
</div>
<div className="flex justify-end gap-2 pt-4 border-t">
<Button onClick={onClose}>
Close
</Button>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

1409
src/PackageDetails.tsx Normal file

File diff suppressed because it is too large Load Diff

265
src/PackageSelector.css Normal file
View File

@@ -0,0 +1,265 @@
@import "@radix-ui/colors/black-alpha.css";
@import "@radix-ui/colors/green.css";
@import "@radix-ui/colors/mauve.css";
@import "@radix-ui/colors/violet.css";
.DialogOverlay {
background-color: rgba(0, 0, 0, 0.6);
position: fixed;
inset: 0;
z-index: 9998;
animation: overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
backdrop-filter: blur(4px);
}
.DialogContent {
background-color: #1a1a1a;
color: #ffffff;
border-radius: 12px;
border: 1px solid #333;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.8);
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90vw;
max-width: 700px;
max-height: 80vh;
padding: 24px;
z-index: 9999;
animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
display: flex;
flex-direction: column;
}
.DialogContent:focus {
outline: none;
}
.DialogTitle {
margin: 0;
font-weight: 600;
color: #ffffff;
font-size: 18px;
}
.DialogDescription {
margin: 8px 0 20px;
color: #a0a0a0;
font-size: 14px;
line-height: 1.5;
}
.PackageSelector {
display: flex;
flex-direction: column;
gap: 16px;
min-height: 0;
}
.SearchBar {
display: flex;
gap: 8px;
}
.SelectionStats {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
font-size: 14px;
color: #b0b0b0;
border-bottom: 1px solid #333;
}
.PackageList {
overflow-y: auto;
max-height: 400px;
display: flex;
flex-direction: column;
gap: 16px;
padding: 4px;
}
.PackageSection {
display: flex;
flex-direction: column;
gap: 4px;
}
.SectionTitle {
font-size: 13px;
font-weight: 600;
color: #b0b0b0;
margin: 8px 0 4px 0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.PackageItem {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: 6px;
cursor: pointer;
transition: background-color 150ms;
border: 1px solid transparent;
}
.PackageItem:hover {
background-color: #2a2a2a;
border-color: #444;
}
.Checkbox {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #3b82f6;
}
.PackageInfo {
display: flex;
flex: 1;
justify-content: space-between;
align-items: center;
gap: 12px;
min-width: 0;
}
.PackageName {
font-family: 'Courier New', monospace;
font-size: 14px;
color: #ffffff;
font-weight: 500;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.PackageVersion {
font-family: 'Courier New', monospace;
font-size: 13px;
color: #a0a0a0;
flex-shrink: 0;
}
.DialogActions {
display: flex;
justify-content: flex-end;
gap: 12px;
padding-top: 16px;
border-top: 1px solid #333;
margin-top: 8px;
}
.Button {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 6px;
padding: 0 16px;
font-size: 14px;
line-height: 1;
font-weight: 500;
height: 36px;
user-select: none;
border: 1px solid #444;
background-color: #2a2a2a;
color: #ffffff;
cursor: pointer;
transition: all 150ms;
}
.Button:hover {
background-color: #333;
border-color: #555;
}
.Button:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
.Button.violet {
background-color: #3b82f6;
color: white;
border-color: #3b82f6;
}
.Button.violet:hover {
background-color: #2563eb;
border-color: #2563eb;
}
.Button.green {
background-color: #22c55e;
color: white;
border-color: #22c55e;
}
.Button.green:hover {
background-color: #16a34a;
border-color: #16a34a;
}
.IconButton {
all: unset;
font-family: inherit;
border-radius: 4px;
height: 24px;
width: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
color: #a0a0a0;
position: absolute;
top: 16px;
right: 16px;
cursor: pointer;
transition: all 150ms;
}
.IconButton:hover {
background-color: #2a2a2a;
color: #ffffff;
}
.IconButton:focus {
box-shadow: 0 0 0 2px #3b82f6;
}
.Input {
width: 100%;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 6px;
padding: 0 12px;
font-size: 14px;
line-height: 1;
color: #ffffff;
background-color: #222;
border: 1px solid #444;
height: 36px;
transition: all 150ms;
}
.Input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
}
@keyframes overlayShow {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes contentShow {
from {
opacity: 0;
transform: translate(-50%, -48%) scale(0.96);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}

171
src/PackageSelector.tsx Normal file
View File

@@ -0,0 +1,171 @@
import * as Dialog from "@radix-ui/react-dialog";
import { Button } from "./components/ui/button";
import { Input } from "./components/ui/input";
import { useState, useEffect } from "react";
import { Search, X, Package2 } from "lucide-react";
import "./PackageSelector.css";
interface PackageData {
name: string;
version: string;
type: 'dependencies' | 'devDependencies';
}
interface PackageSelectorProps {
open: boolean;
packages: PackageData[];
selectedPackages: Set<string>;
onConfirm: (selected: Set<string>) => void;
onCancel: () => void;
}
export function PackageSelector({ open, packages, selectedPackages, onConfirm, onCancel }: PackageSelectorProps) {
const [localSelected, setLocalSelected] = useState<Set<string>>(new Set());
const [searchTerm, setSearchTerm] = useState("");
useEffect(() => {
setLocalSelected(new Set(selectedPackages));
}, [selectedPackages, open]);
const togglePackage = (name: string) => {
const newSelected = new Set(localSelected);
if (newSelected.has(name)) {
newSelected.delete(name);
} else {
newSelected.add(name);
}
setLocalSelected(newSelected);
};
const toggleAll = () => {
if (localSelected.size === filteredPackages.length) {
setLocalSelected(new Set());
} else {
setLocalSelected(new Set(filteredPackages.map(p => p.name)));
}
};
const handleConfirm = () => {
onConfirm(localSelected);
};
const filteredPackages = packages.filter(pkg =>
pkg.name.toLowerCase().includes(searchTerm.toLowerCase())
);
const deps = filteredPackages.filter(p => p.type === 'dependencies');
const devDeps = filteredPackages.filter(p => p.type === 'devDependencies');
return (
<Dialog.Root open={open} onOpenChange={(isOpen) => !isOpen && onCancel()}>
<Dialog.Portal>
<Dialog.Overlay className="DialogOverlay" />
<Dialog.Content className="DialogContent" style={{ maxWidth: '700px', maxHeight: '85vh' }}>
<div className="flex items-start justify-between mb-4">
<div>
<Dialog.Title className="text-xl font-semibold">Select Packages</Dialog.Title>
<Dialog.Description className="text-sm text-muted-foreground mt-1">
Choose which packages to include in compatibility analysis
</Dialog.Description>
</div>
<Dialog.Close asChild>
<button className="rounded-md p-1.5 hover:bg-accent transition-colors" aria-label="Close">
<X className="h-4 w-4" />
</button>
</Dialog.Close>
</div>
<div className="PackageSelector">
<div className="space-y-4">
{/* Search Bar */}
<div className="relative">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="text"
placeholder="Search packages..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
{/* Selection Stats */}
<div className="flex items-center justify-between p-3 rounded-md bg-muted/50">
<span className="text-sm font-medium">{localSelected.size} of {packages.length} selected</span>
<Button onClick={toggleAll} variant="outline" size="sm">
{localSelected.size === filteredPackages.length ? 'Deselect' : 'Select'} All
</Button>
</div>
</div>
<div className="PackageList max-h-[400px] overflow-y-auto border rounded-md">
{deps.length > 0 && (
<div className="PackageSection">
<div className="sticky top-0 z-10 bg-background p-3 border-b">
<h4 className="text-sm font-semibold flex items-center gap-2">
<Package2 className="h-4 w-4 text-muted-foreground" />
Dependencies ({deps.length})
</h4>
</div>
<div className="p-2 space-y-1">
{deps.map(pkg => (
<label key={pkg.name} className="flex items-center gap-3 p-2.5 hover:bg-accent rounded-md cursor-pointer transition-colors">
<input
type="checkbox"
checked={localSelected.has(pkg.name)}
onChange={() => togglePackage(pkg.name)}
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-2 focus:ring-primary"
/>
<div className="flex-1 min-w-0">
<div className="font-mono text-sm truncate">{pkg.name}</div>
<div className="text-xs text-muted-foreground">{pkg.version}</div>
</div>
</label>
))}
</div>
</div>
)}
{devDeps.length > 0 && (
<div className="PackageSection">
<div className="sticky top-0 z-10 bg-background p-3 border-b">
<h4 className="text-sm font-semibold flex items-center gap-2">
<Package2 className="h-4 w-4 text-muted-foreground" />
Dev Dependencies ({devDeps.length})
</h4>
</div>
<div className="p-2 space-y-1">
{devDeps.map(pkg => (
<label key={pkg.name} className="flex items-center gap-3 p-2.5 hover:bg-accent rounded-md cursor-pointer transition-colors">
<input
type="checkbox"
checked={localSelected.has(pkg.name)}
onChange={() => togglePackage(pkg.name)}
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-2 focus:ring-primary"
/>
<div className="flex-1 min-w-0">
<div className="font-mono text-sm truncate">{pkg.name}</div>
<div className="text-xs text-muted-foreground">{pkg.version}</div>
</div>
</label>
))}
</div>
</div>
)}
</div>
{/* Actions */}
<div className="flex justify-end gap-2 pt-4">
<Button onClick={onCancel} variant="outline">
Cancel
</Button>
<Button onClick={handleConfirm}>
Confirm Selection
</Button>
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

View File

@@ -0,0 +1,52 @@
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} />;
}
export { Button, buttonVariants };

View File

@@ -0,0 +1,56 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn("bg-card text-card-foreground flex flex-col gap-3 rounded-xl border py-4 shadow-sm", className)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className,
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-title" className={cn("leading-none font-semibold", className)} {...props} />;
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-description" className={cn("text-muted-foreground text-sm", className)} {...props} />;
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-content" className={cn("px-6", className)} {...props} />;
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div data-slot="card-footer" className={cn("flex items-center px-6 [.border-t]:pt-6", className)} {...props} />
);
}
export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };

View File

@@ -0,0 +1,21 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className,
)}
{...props}
/>
);
}
export { Input };

View File

@@ -0,0 +1,21 @@
"use client";
import * as LabelPrimitive from "@radix-ui/react-label";
import * as React from "react";
import { cn } from "@/lib/utils";
function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className,
)}
{...props}
/>
);
}
export { Label };

View File

@@ -0,0 +1,162 @@
"use client";
import * as SelectPrimitive from "@radix-ui/react-select";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default";
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
}
function SelectContent({
className,
children,
position = "popper",
align = "center",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
}
function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
);
}
function SelectItem({ className, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className,
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
function SelectSeparator({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function SelectScrollUpButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
);
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
);
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
};

View File

@@ -0,0 +1,18 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
{...props}
/>
);
}
export { Textarea };

View File

@@ -0,0 +1,27 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

20
src/frontend.tsx Normal file
View File

@@ -0,0 +1,20 @@
/**
* This file is the entry point for the React app, it sets up the root
* element and renders the App component to the DOM.
*
* It is included in `src/index.html`.
*/
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App";
const elem = document.getElementById("root")!;
const app = (
<StrictMode>
<App />
</StrictMode>
);
// https://bun.com/docs/bundler/hot-reloading#import-meta-hot-data
(import.meta.hot.data.root ??= createRoot(elem)).render(app);

93
src/index.css Normal file
View File

@@ -0,0 +1,93 @@
@import "../styles/globals.css";
@layer base {
:root {
@apply font-sans;
}
body {
@apply min-w-[320px] min-h-screen relative m-0 bg-background text-foreground;
}
}
/* cool Bun background animation 😎 */
body::before {
content: "";
position: fixed;
inset: 0;
z-index: -1;
opacity: 0.05;
background: url("./logo.svg");
background-size: 256px;
transform: rotate(-12deg) scale(1.35);
animation: slide 30s linear infinite;
pointer-events: none;
}
@keyframes slide {
from {
background-position: 0 0;
}
to {
background-position: 256px 224px;
}
}
@keyframes spin {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
/* Radix UI Themes Spinner animation fix */
.rt-Spinner {
animation: rt-spinner-linear-spin 1s linear infinite !important;
}
@keyframes rt-spinner-linear-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Collapsible animations */
.CollapsibleContent {
overflow: hidden;
}
.CollapsibleContent[data-state='open'] {
animation: slideDown 300ms cubic-bezier(0.87, 0, 0.13, 1);
}
.CollapsibleContent[data-state='closed'] {
animation: slideUp 300ms cubic-bezier(0.87, 0, 0.13, 1);
}
@keyframes slideDown {
from {
height: 0;
opacity: 0;
}
to {
height: var(--radix-collapsible-content-height);
opacity: 1;
}
}
@keyframes slideUp {
from {
height: var(--radix-collapsible-content-height);
opacity: 1;
}
to {
height: 0;
opacity: 0;
}
}

13
src/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="./logo.svg" />
<title>npm Dependency Analyzer</title>
<script type="module" src="./frontend.tsx" async></script>
</head>
<body>
<div id="root"></div>
</body>
</html>

45
src/index.ts Normal file
View File

@@ -0,0 +1,45 @@
import { serve } from "bun";
import index from "./index.html";
import { getPackageDetails } from "./lib/npm_registry";
import { fetchAllPackageDetails } from "./services/api";
const server = serve({
routes: {
// Serve index.html for all unmatched routes.
"/*": index,
"/api/analyze": async req => {
const data = await req.json();
const dependencies = data.dependencies || {};
const devDependencies = data.devDependencies || {};
let devDependeciesPackagetDetails: any[] = [];
let dependenciesPackagetDetails: any[] = [];
try {
devDependeciesPackagetDetails = await fetchAllPackageDetails(devDependencies);
dependenciesPackagetDetails = await fetchAllPackageDetails(dependencies);
} catch (error) {
console.log(error);
devDependeciesPackagetDetails = [];
dependenciesPackagetDetails = [];
}
return Response.json({
data: {
devDependencies: devDependeciesPackagetDetails,
dependencies: dependenciesPackagetDetails
}
});
},
},
development: process.env.NODE_ENV !== "production" && {
// Enable browser hot reloading in development
hmr: true,
// Echo console logs from the browser to the server
console: true,
},
});
console.log(`Server running at ${server.url}`);

31
src/lib/npm_registry.ts Normal file
View File

@@ -0,0 +1,31 @@
const API_BASE_URL = "https://registry.npmjs.org/";
export async function getPackageDetails(packageName: string) {
const packageUrl = `${API_BASE_URL}${packageName}`;
return fetch(packageUrl)
.then(res => {
if (res.status === 404) {
return {
name: packageName,
error: "not-found",
isPrivatePackage: true,
message: "Package not found in npm registry - may be a private package"
};
}
if (!res.ok) {
return {
name: packageName,
error: "fetch-error",
message: `Failed to fetch package (Status: ${res.status})`
};
}
return res.json();
}).catch(error => {
console.error(`Error fetching package ${packageName}:`, error);
return {
name: packageName,
error: "network-error",
message: error.message || "Network error while fetching package"
};
});
}

6
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

1
src/logo.svg Normal file
View File

@@ -0,0 +1 @@
<svg id="Bun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 70"><title>Bun Logo</title><path id="Shadow" d="M71.09,20.74c-.16-.17-.33-.34-.5-.5s-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5A26.46,26.46,0,0,1,75.5,35.7c0,16.57-16.82,30.05-37.5,30.05-11.58,0-21.94-4.23-28.83-10.86l.5.5.5.5.5.5.5.5.5.5.5.5.5.5C19.55,65.3,30.14,69.75,42,69.75c20.68,0,37.5-13.48,37.5-30C79.5,32.69,76.46,26,71.09,20.74Z"/><g id="Body"><path id="Background" d="M73,35.7c0,15.21-15.67,27.54-35,27.54S3,50.91,3,35.7C3,26.27,9,17.94,18.22,13S33.18,3,38,3s8.94,4.13,19.78,10C67,17.94,73,26.27,73,35.7Z" style="fill:#fbf0df"/><path id="Bottom_Shadow" data-name="Bottom Shadow" d="M73,35.7a21.67,21.67,0,0,0-.8-5.78c-2.73,33.3-43.35,34.9-59.32,24.94A40,40,0,0,0,38,63.24C57.3,63.24,73,50.89,73,35.7Z" style="fill:#f6dece"/><path id="Light_Shine" data-name="Light Shine" d="M24.53,11.17C29,8.49,34.94,3.46,40.78,3.45A9.29,9.29,0,0,0,38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7c0,.4,0,.8,0,1.19C9.06,15.48,20.07,13.85,24.53,11.17Z" style="fill:#fffefc"/><path id="Top" d="M35.12,5.53A16.41,16.41,0,0,1,29.49,18c-.28.25-.06.73.3.59,3.37-1.31,7.92-5.23,6-13.14C35.71,5,35.12,5.12,35.12,5.53Zm2.27,0A16.24,16.24,0,0,1,39,19c-.12.35.31.65.55.36C41.74,16.56,43.65,11,37.93,5,37.64,4.74,37.19,5.14,37.39,5.49Zm2.76-.17A16.42,16.42,0,0,1,47,17.12a.33.33,0,0,0,.65.11c.92-3.49.4-9.44-7.17-12.53C40.08,4.54,39.82,5.08,40.15,5.32ZM21.69,15.76a16.94,16.94,0,0,0,10.47-9c.18-.36.75-.22.66.18-1.73,8-7.52,9.67-11.12,9.45C21.32,16.4,21.33,15.87,21.69,15.76Z" style="fill:#ccbea7;fill-rule:evenodd"/><path id="Outline" d="M38,65.75C17.32,65.75.5,52.27.5,35.7c0-10,6.18-19.33,16.53-24.92,3-1.6,5.57-3.21,7.86-4.62,1.26-.78,2.45-1.51,3.6-2.19C32,1.89,35,.5,38,.5s5.62,1.2,8.9,3.14c1,.57,2,1.19,3.07,1.87,2.49,1.54,5.3,3.28,9,5.27C69.32,16.37,75.5,25.69,75.5,35.7,75.5,52.27,58.68,65.75,38,65.75ZM38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7,3,50.89,18.7,63.25,38,63.25S73,50.89,73,35.7C73,26.62,67.31,18.13,57.78,13,54,11,51.05,9.12,48.66,7.64c-1.09-.67-2.09-1.29-3-1.84C42.63,4,40.42,3,38,3Z"/></g><g id="Mouth"><g id="Background-2" data-name="Background"><path d="M45.05,43a8.93,8.93,0,0,1-2.92,4.71,6.81,6.81,0,0,1-4,1.88A6.84,6.84,0,0,1,34,47.71,8.93,8.93,0,0,1,31.12,43a.72.72,0,0,1,.8-.81H44.26A.72.72,0,0,1,45.05,43Z" style="fill:#b71422"/></g><g id="Tongue"><path id="Background-3" data-name="Background" d="M34,47.79a6.91,6.91,0,0,0,4.12,1.9,6.91,6.91,0,0,0,4.11-1.9,10.63,10.63,0,0,0,1-1.07,6.83,6.83,0,0,0-4.9-2.31,6.15,6.15,0,0,0-5,2.78C33.56,47.4,33.76,47.6,34,47.79Z" style="fill:#ff6164"/><path id="Outline-2" data-name="Outline" d="M34.16,47a5.36,5.36,0,0,1,4.19-2.08,6,6,0,0,1,4,1.69c.23-.25.45-.51.66-.77a7,7,0,0,0-4.71-1.93,6.36,6.36,0,0,0-4.89,2.36A9.53,9.53,0,0,0,34.16,47Z"/></g><path id="Outline-3" data-name="Outline" d="M38.09,50.19a7.42,7.42,0,0,1-4.45-2,9.52,9.52,0,0,1-3.11-5.05,1.2,1.2,0,0,1,.26-1,1.41,1.41,0,0,1,1.13-.51H44.26a1.44,1.44,0,0,1,1.13.51,1.19,1.19,0,0,1,.25,1h0a9.52,9.52,0,0,1-3.11,5.05A7.42,7.42,0,0,1,38.09,50.19Zm-6.17-7.4c-.16,0-.2.07-.21.09a8.29,8.29,0,0,0,2.73,4.37A6.23,6.23,0,0,0,38.09,49a6.28,6.28,0,0,0,3.65-1.73,8.3,8.3,0,0,0,2.72-4.37.21.21,0,0,0-.2-.09Z"/></g><g id="Face"><ellipse id="Right_Blush" data-name="Right Blush" cx="53.22" cy="40.18" rx="5.85" ry="3.44" style="fill:#febbd0"/><ellipse id="Left_Bluch" data-name="Left Bluch" cx="22.95" cy="40.18" rx="5.85" ry="3.44" style="fill:#febbd0"/><path id="Eyes" d="M25.7,38.8a5.51,5.51,0,1,0-5.5-5.51A5.51,5.51,0,0,0,25.7,38.8Zm24.77,0A5.51,5.51,0,1,0,45,33.29,5.5,5.5,0,0,0,50.47,38.8Z" style="fill-rule:evenodd"/><path id="Iris" d="M24,33.64a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,24,33.64Zm24.77,0a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,48.75,33.64Z" style="fill:#fff;fill-rule:evenodd"/></g></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

8
src/react.svg Normal file
View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-11.5 -10.23174 23 20.46348">
<circle cx="0" cy="0" r="2.05" fill="#61dafb"/>
<g stroke="#61dafb" stroke-width="1" fill="none">
<ellipse rx="11" ry="4.2"/>
<ellipse rx="11" ry="4.2" transform="rotate(60)"/>
<ellipse rx="11" ry="4.2" transform="rotate(120)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 338 B

6
src/services/api.ts Normal file
View File

@@ -0,0 +1,6 @@
import { getPackageDetails } from "@/lib/npm_registry";
export function fetchAllPackageDetails(dependencies: Record<string, string>): Promise<any[]> {
const packageDetailsPromises = Object.keys(dependencies).map(dep => getPackageDetails(dep));
return Promise.all(packageDetailsPromises);
}