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 usingresolveRegistryItemsFromRegistrieswith 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:
dependenciesanddevDependencies(deep‑merged)files(deduplicated by target path viadeduplicateFilesByTarget)- 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
resolveRegistryTreeinpackages/shadcn/src/registry/resolver.ts, which fetches component definitions and their metadata. - Recursive resolution via
resolveDependenciesRecursivelyhandles 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
addComponentsvalidates 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →