How shadcn Resolves Component Dependencies and Builds Installation Trees

shadcn‑ui's CLI resolves component dependencies by recursively fetching registry definitions, topologically sorting the results using Kahn's algorithm, and merging them into a single installation tree that guarantees dependencies are installed before their dependents.

The shadcn CLI automates component installation by analyzing dependency graphs across the registry. When you run npx shadcn add button, the tool performs complex dependency tree resolution to ensure that underlying primitives, hooks, and utility files are installed automatically. Understanding how shadcn resolves component dependencies reveals the deterministic pipeline that powers the add command.

The Dependency Resolution Pipeline

The core resolution logic lives in packages/shadcn/src/registry/resolver.ts. The process begins with resolveRegistryTree, which orchestrates three distinct phases: fetching initial items, recursively resolving nested dependencies, and sorting the final payload.

Fetching Initial Registry Items

The entry point resolveRegistryTree receives an array of component names and the project configuration. It deduplicates the input and fetches the corresponding JSON definitions from the registry.

export async function resolveRegistryTree(
  names: RegistryItem["name"][],
  config: Config,
  options: { useCache?: boolean } = {}
) {
  const uniqueNames = Array.from(new Set(names))
  const results = await fetchRegistryItems(uniqueNames, config, options)
  // results → parsed RegistryItem objects
}

Source: packages/shadcn/src/registry/resolver.ts (lines 24‑34).

The helper fetchRegistryItems routes each identifier through different resolution strategies:

  • Local files: Resolved via fetchRegistryLocal
  • Direct URLs: Fetched with a single HTTP request
  • Namespaced items (e.g., @custom/ui/card): Converted to full URLs using resolveRegistryItemsFromRegistries with custom headers from config
  • Plain component names: Treated as style-scoped items (styles/${style}/${name}.json)

Source: packages/shadcn/src/registry/resolver.ts (lines 68‑100).

Recursively Resolving Dependencies

Once an item is fetched, resolveDependenciesRecursively inspects its registryDependencies array. This function handles three identifier types and builds two collections: resolved registry items and pending registry names that require URL resolution.

async function resolveDependenciesRecursively(
  dependencies: string[],
  config: Config,
  options: { useCache?: boolean } = {},
  visited: Set<string> = new Set()
) {
  for (const dep of dependencies) {
    if (dep.startsWith("@") && config?.registries) {
      // Namespaced registry item → fetch with config headers
      const [item] = await fetchRegistryItems([dep], config, options)
      // Recurse into its own registryDependencies
    } else if (isUrl(dep) || isLocalFile(dep)) {
      // Direct URL or local JSON → fetch and recurse
    } else {
      // Plain component name → queue for style-based resolution
    }
  }
}

Source: packages/shadcn/src/registry/resolver.ts (lines 66‑124).

A visited Set prevents infinite loops when circular dependencies exist. Namespaced items trigger a header-aware fetch, throwing RegistryNotConfiguredError if the registry is undefined in the config. Plain component names are collected in a registryNames array for batch processing.

Resolving Plain Names to URLs

After the recursive walk collects all plain component names, resolveRegistryDependencies converts them to final URLs. It determines the target style from the configuration and maps each name to a registry endpoint.

async function resolveRegistryDependencies(url: string, config: Config) {
  const { registryNames } = await resolveDependenciesRecursively(
    [url], config, {}, new Set()
  )
  const style = await getTargetStyleFromConfig(config)
  const urls = registryNames.map(name => 
    resolveRegistryUrl(`styles/${style}/${name}.json`)
  )
  return Array.from(new Set(urls))
}

Source: packages/shadcn/src/registry/resolver.ts (lines 75‑99).

Building the Installation Tree

After gathering all items, the resolver constructs a deterministic installation tree through hashing, topological sorting, and deep merging.

Topological Sorting with Kahn's Algorithm

Each item receives a deterministic hash via computeItemHash that incorporates its name and source URL. This ensures that the same component from different registries is treated as distinct. The resolver then builds a dependency graph and applies topologicalSortRegistryItems, which implements Kahn's algorithm to produce a linear order where dependencies always precede dependents.

payload = topologicalSortRegistryItems(payload, sourceMap)

Source: packages/shadcn/src/registry/resolver.ts (lines 22‑24, 86‑142).

If a circular dependency is detected, the algorithm emits a warning and appends the remaining items in arbitrary order (lines 22‑41).

Merging and Deduplicating Resources

The sorted payload is merged into a single registryResolvedItemsTree structure. The resolver aggregates:

  • dependencies and devDependencies (deep‑merged)
  • files (deduplicated by target path via deduplicateFilesByTarget)
  • Tailwind configuration, CSS variables, CSS, documentation, and fonts

The final structure conforms to registryResolvedItemsTreeSchema:

{
  dependencies: { /* package names and versions */ },
  devDependencies: { /* package names and versions */ },
  files: [{ path, type, target }, …],
  tailwind: { /* config extensions */ },
  cssVars: { /* variable definitions */ },
  css: { /* custom styles */ },
  docs: "markdown content",
  fonts: [{ name, type: "registry:font", font: … }]
}

Source: packages/shadcn/src/registry/resolver.ts (lines 46‑58, 148‑158).

CLI Integration and Execution

The addComponents function in packages/shadcn/src/utils/add-components.ts serves as the CLI entry point. It invokes resolveRegistryTree, validates the generated files, and executes a series of updaters on the merged tree.

let tree = await resolveRegistryTree(components, configWithDefaults(config))
await updateFiles(tree.files, config, { overwrite, silent })
await updateDependencies(tree.dependencies, tree.devDependencies, config, { silent })

Source: packages/shadcn/src/utils/add-components.ts (lines 84‑102, 124‑132).

This ensures that when you install a component, all Tailwind configs, CSS variables, and file dependencies are applied in the correct order.

Practical Code Examples

Resolving a Component from the CLI

When you execute:

npx shadcn add button

The CLI internally calls:

const tree = await resolveRegistryTree(
  ["button"],
  configWithDefaults(config)
)
// tree contains button + all registryDependencies

Source: packages/shadcn/src/utils/add-components.ts (line 84) and packages/shadcn/src/registry/resolver.ts (line 24).

Using the Resolver Programmatically

You can invoke the resolver directly in Node.js to inspect dependencies before installation:

import { resolveRegistryTree } from "@/src/registry/resolver"
import { configWithDefaults } from "@/src/registry/config"

async function getTree() {
  const cfg = await configWithDefaults(/* your config */)
  const tree = await resolveRegistryTree(
    ["alert-dialog", "@custom/ui/card"],
    cfg,
    { useCache: false }
  )
  console.log(tree.files) // Files to be written
  console.log(tree.dependencies) // NPM packages required
}

Source: packages/shadcn/src/registry/resolver.ts (lines 24‑34, 88‑140).

Inspecting Dependency Order

To verify the exact installation sequence:

import { topologicalSortRegistryItems } from "@/src/registry/resolver"

const sorted = topologicalSortRegistryItems(payload, sourceMap)
sorted.forEach(item => console.log(item.name))

The output lists items in the precise order the CLI applies updates, ensuring that every dependency is installed before the component that requires it.

Source: packages/shadcn/src/registry/resolver.ts (lines 86‑142).

Summary

  • Dependency discovery begins with resolveRegistryTree in packages/shadcn/src/registry/resolver.ts, which fetches component definitions and their metadata.
  • Recursive resolution via resolveDependenciesRecursively handles namespaced items (@registry/name), direct URLs, local files, and plain component names while preventing circular dependencies using a visited Set.
  • Topological sorting applies Kahn's algorithm to ensure dependencies always appear before dependents in the installation queue.
  • Tree merging aggregates all configured resources—files, NPM dependencies, Tailwind config, and CSS variables—into a single registryResolvedItemsTree.
  • CLI execution in addComponents validates and applies the resolved tree to the project filesystem.

Frequently Asked Questions

How does shadcn handle circular dependencies?

The resolver passes a visited Set through resolveDependenciesRecursively to track already-processed items. If a circular reference is detected during topological sorting, Kahn's algorithm emits a warning and appends the remaining items in arbitrary order rather than failing.

What is the difference between namespaced and plain component names?

Namespaced items follow the @registry/name pattern and require a matching registry configuration in components.json, allowing custom headers and endpoints. Plain component names are resolved against the default shadcn registry using the project's configured style (e.g., styles/new-york/button.json).

How does the CLI ensure dependencies are installed before dependents?

After fetching all registry items, the resolver assigns each a unique hash and builds a dependency graph. It then runs topologicalSortRegistryItems, which uses Kahn's algorithm to produce a linear ordering where every component appears after its registryDependencies entries.

Can I resolve dependencies without installing them?

Yes. Import resolveRegistryTree from packages/shadcn/src/registry/resolver.ts and call it with your configuration. This returns the full registryResolvedItemsTree containing all files, dependencies, and configuration updates without modifying your project, allowing you to preview the installation impact.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →