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:
64
src/APITester.tsx
Normal file
64
src/APITester.tsx
Normal 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
26
src/App.tsx
Normal 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;
|
||||
171
src/CompatibilityOverview.tsx
Normal file
171
src/CompatibilityOverview.tsx
Normal 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
1409
src/PackageDetails.tsx
Normal file
File diff suppressed because it is too large
Load Diff
265
src/PackageSelector.css
Normal file
265
src/PackageSelector.css
Normal 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
171
src/PackageSelector.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
52
src/components/ui/button.tsx
Normal file
52
src/components/ui/button.tsx
Normal 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 };
|
||||
56
src/components/ui/card.tsx
Normal file
56
src/components/ui/card.tsx
Normal 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 };
|
||||
21
src/components/ui/input.tsx
Normal file
21
src/components/ui/input.tsx
Normal 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 };
|
||||
21
src/components/ui/label.tsx
Normal file
21
src/components/ui/label.tsx
Normal 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 };
|
||||
162
src/components/ui/select.tsx
Normal file
162
src/components/ui/select.tsx
Normal 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,
|
||||
};
|
||||
18
src/components/ui/textarea.tsx
Normal file
18
src/components/ui/textarea.tsx
Normal 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 };
|
||||
27
src/components/ui/tooltip.tsx
Normal file
27
src/components/ui/tooltip.tsx
Normal 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
20
src/frontend.tsx
Normal 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
93
src/index.css
Normal 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
13
src/index.html
Normal 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
45
src/index.ts
Normal 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
31
src/lib/npm_registry.ts
Normal 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
6
src/lib/utils.ts
Normal 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
1
src/logo.svg
Normal 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
8
src/react.svg
Normal 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
6
src/services/api.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user