Display

Dependency List

An auditable dependency tree with real semver comparison, graded staleness, ordered CVE severities, licence obligation classes and import paths.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import { AlertCircle, ChevronRight, CornerDownRight, ExternalLink, Package, Search, ShieldAlert } from "lucide-react"
import { cn } from "@/lib/utils"
import {
  DEPENDENCY_SEVERITY_ORDER,
  type DependencyAdvisory,
  type DependencyListStatus,
  type DependencyRow,
  type DependencySeverity,
} from "./dependency-list.contract"

/* ------------------------------------------------------------------ *

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/dependency-list.json

Prompt

Build a React + TypeScript + Tailwind "DependencyList" component using zod,
lucide-react and the local cn() utility.

Contract (dependency-list.contract.ts)
- dependencySeveritySchema = z.enum(["critical","high","moderate","low"]),
  declared worst-first from a DEPENDENCY_SEVERITY_ORDER tuple so the ordering
  is data rather than a second hardcoded ranking.
- dependencyAdvisorySchema = { id, severity, title, url, patchedIn? }. `url`
  is refined to an absolute http(s) form — `#`, `""` and relative paths are
  rejected at the contract boundary so a dead advisory anchor can never reach
  the DOM.
- dependencyRowSchema = { id, name, range?, installed, latest?, license?,
  relation: "direct" | "transitive", via?: string[], sizeBytes?, advisories?,
  homepage? } with a refine: a transitive row must carry a non-empty `via`
  chain, because "transitive" without the chain that introduced it is not
  actionable. `latest`, `license` and `via` are optional so "not reported"
  stays representable and distinct from "fine".
- dependencyListSchema = { status: "loading"|"empty"|"error"|"ready", items }.
- Props extend the inferred row array with status, label, locale ("en-US"),
  errorMessage, onRetry?, skeletonRows (clamped 1–24), defaultSort,
  defaultScope, className and native section attributes; forwardRef<HTMLElement>.

Behavior — version arithmetic (the part that must not be faked)
- Parse versions with a real semver regex (optional leading v, discard build
  metadata) and compare by precedence, never as strings. Text comparison ranks
  1.10.0 below 1.9.0 and ranks 2.0.0-beta.1 above 2.0.0 — the two most common
  upgrade situations, both reported backwards.
- Prerelease rules follow semver: a release outranks any prerelease of the same
  core; numeric identifiers compare numerically and rank below alphanumeric
  ones; a shorter identifier list ranks lower.
- Grade staleness instead of emitting one "update available":
  current / prerelease / patch / minor / major, and carry the major gap so
  "3 majors behind" is distinguishable from "1 major behind". A version that
  fails to parse, or a missing `latest`, yields "unknown" — never "up to date".
- Evaluate the declared range so the row answers "does npm update reach this?":
  caret is major-locked above 0.x, minor-locked on 0.x and patch-locked on
  0.0.x; tilde is minor-locked; `>=` is open; `*` / `x` / empty accept
  anything. Apply npm's prerelease rule (a prerelease only satisfies a
  comparator that names a prerelease on the same core). Unmodelled syntax
  returns "unknown" rather than a guess.

Behavior — risk
- Sort each row's advisories worst-first and take the worst as the row's
  severity. Severity is shown with the word, a stair-step pip meter and the pip
  count; hue is the last channel, and critical/high deliberately share it so
  nothing depends on telling two reds apart.
- Every advisory links to its real source in a new tab with
  rel="noreferrer noopener" and an sr-only "opens in a new tab".
- Classify licences into obligation classes: permissive, weak-copyleft,
  strong-copyleft, proprietary, unknown. The `-only` / `-or-later` / `+`
  suffixes fold onto the base id. SPDX expressions: a pure OR lets you take the
  least demanding alternative, anything else stacks every obligation. An absent
  licence, NOASSERTION or "SEE LICENSE IN …" is `unknown` — its own class,
  ranked above permissive and weak copyleft in the risk sort, drawn with a
  dashed chip reading "unreviewed". It must never render as cleared.
- Transitive rows render the full import chain as a wrapping paragraph
  ("via a to b to c"), with no truncation, no max-height and no scroller: a
  deep chain is exactly when every hop matters.

Behavior — filtering and sorting
- Filters are real and compose: substring search over name *and* import chain,
  scope (all / direct / transitive), "vulnerable only" and "outdated only"
  toggles whose badge counts come from the same derivation as the rows.
- Sorts: severity-worst-first (ties broken by advisory count, then licence
  class, then staleness), most-outdated-first (level then major gap), name, and
  largest install first. Every sort ends with a source-index tiebreak so the
  order is deterministic.
- Filtering to zero renders its own panel ("No packages match these filters"
  plus a Clear filters button), textually distinct from status="empty" ("No
  dependencies recorded"). A persistent polite live region reports
  "Showing M of N packages".
- Never cap the rendered rows, advisories or path hops.

Rendering & styling
- Semantic tokens only: bg-card, bg-muted, bg-secondary, bg-background,
  bg-primary/10, bg-destructive/10 and /15, text-muted-foreground,
  text-destructive, border and ring. No hex, no rgb(), no oklch().
- No breakpoints at all, viewport or container — every row is a wrapping flex
  line, so the same list is a full page in one app and a 320px sidebar in
  another. Do not reach for @container out of habit: with no @-variants to feed
  it, container-type: inline-size only stops the card sizing to its own content
  inside a w-fit or auto-width parent. Long package names and path hops carry
  both `wrap-anywhere` and `min-w-0`; verify by removing each one separately,
  since either alone can be the load-bearing half.
- Native select elements for scope and sort, with [&>option] token colours so
  the popup is readable in dark mode; toggles are buttons with aria-pressed;
  focus-visible:ring-2 ring-ring throughout; decorative glyphs aria-hidden.
- Skeletons pulse with motion-reduce:animate-none and sit behind a single
  role="status" line.

Customization levers
- Row density: the three row lines (identity / versions / provenance) are
  independent blocks — drop the size column or the range chip without touching
  the derivations.
- Licence policy: LICENSE_TABLE and LICENSE_RANK are the only place obligation
  classes live; add SPDX ids or re-rank classes there. Keep `unknown` above the
  cleared classes.
- Severity vocabulary: extend the enum tuple and the label/class maps together;
  the pip meter reads its length from the tuple.
- Sorting: DEPENDENCY_SORT_LABELS drives the select, so adding a sort is one
  comparator plus one label.
- Provenance: swap the wrapping chain for a collapsible tree if your chains
  routinely exceed a dozen hops — but keep it expandable to full length rather
  than truncating.
- Data reuse: analyzeDependency(row) returns the whole derivation (level,
  majorGap, rangeFit, licence risk, worst severity) for a table, a CI summary
  or an export that has to agree with the UI.

Concepts

  • Semver precedence — versions are compared field by field with semver's prerelease rules, because lexicographic order puts 1.10.0 below 1.9.0 and 2.0.0-beta.1 above 2.0.0.
  • Graded staleness — one patch behind, one minor behind and three majors behind are different amounts of work, so they get different labels; an unparseable version or a missing latest becomes "unknown", never "up to date".
  • Range fit — the declared ^ / ~ range decides whether the newest release is reachable by npm update or needs a manifest edit, and npm's prerelease rule is applied so a caret never silently swallows an alpha.
  • Ordered severity — the four advisory levels are ranked, and the ranking is carried by a word plus a stair-step meter so it survives greyscale, colour blindness and a printed report.
  • Unknown is not safe — an undeclared licence gets its own class, ranked above the cleared ones, and a dashed "unreviewed" chip; "we did not check" and "there is nothing to worry about" are opposite claims.
  • Import-path provenance — a transitive row is only actionable with the chain that pulled it in, so the contract requires that chain and the row wraps it to full length instead of truncating.
  • Filter then sort — filters narrow, sorts order, and both end in a source-index tiebreak; the filtered-empty panel is worded differently from the genuinely empty tree and always offers a way back.

On This Page