The registry
Where components come from, how a version is pinned, and how to serve your own.
The registry is committed to the repository and read straight off raw.githubusercontent.com.
There is no service to host and nothing to keep running — and nothing to be down when you run
add.
registry/
├── registry.json the index: names, descriptions, dependencies
├── r/button.json one item: what it is, what it needs, which files
└── config.schema.json the JSON Schema behind native-components.json's $schemaThat is the whole registry. It holds no source, because it does not need to: an item references the library's own files, at the same ref, and the client fetches them from there.
packages/react-native-ui/src/components/button/button.tsx what r/button.json namesshadcn inlines each file as a string inside the item JSON; the cost of that is a megabyte of
escaped source that no reviewer can read and git cannot diff. Copying the source into the
registry instead diffs readably but duplicates the library — two hundred files that exist only to
be kept in step. Naming the library file is both: you can open it on GitHub and see exactly what
add is about to copy, and there is only ever one of it.
add button resolves the graph from the index, fetches the handful of items it needs, then their
files. Everything fetched is cached under your cache directory and revalidated with an ETag, so a
repeated add costs a 304 and an add on a flaky connection still works from what is already
there. The ref pins the whole tree at once, so an item and its files can never disagree.
An item
{
"name": "button",
"type": "registry:ui",
"title": "Button",
"description": "A pressable action composed from parts, with variants, sizes and a loading state.",
"registryDependencies": ["icon", "pressable", "spinner", "text", "tv"],
"dependencies": ["tailwind-variants"],
"expoDependencies": [],
"devDependencies": [],
"files": [
{
"path": "packages/react-native-ui/src/components/button/button.tsx",
"target": "button/button.tsx",
"namespace": "ui",
"rewrites": [{ "from": "../icon", "to": "@registry/ui/icon" }]
}
]
}path is the file itself, in the repository the item was read from; namespace and target are
where it lands in your project. rewrites is how the library's own relative imports become
portable — see Building your own — and is absent for a file that imports
nothing across a directory. A file that inlines content is rejected rather than ignored, so a
shadcn-shaped item fails with a message rather than a 404 halfway through a copy.
The dependency list is split three ways on purpose, and it is the split that decides whether an Expo build compiles:
Prop
Type
Why expoDependencies is its own field
Expo pins every native module to a version its SDK can build. bun add react-native-reanimated
fetches the newest release instead, which on any older SDK is a package that fails at the linker
rather than at install time. A single dependencies field cannot express that difference.
The site reads these fields too: the package install list on the Installation pages is the union of every item's package lists, so the packages it tells you to install are the ones the components actually import. Nothing there is transcribed.
Each item also carries the component's AGENTS.md, rewritten to cite your paths rather than the
package's — so an agent working in your repository gets the design rules next to the code.
Pinning a version
The CLI is published with the git ref it was built from baked in, so a given version always reads the registry it shipped against. Override it two ways, in this order:
delacour add tabs --ref develop # 1. the flag{ "registry": { "ref": "v0.2.0" } } // 2. the configThe ref a file was copied from is recorded in the
lock file, so a later update knows what to merge
from whichever ref you are on by then.
--ref develop is how you take a component that has landed since your CLI was published —
main only moves on a release, so it holds nothing your newest CLI does not already read. bunx delacour@latest gets you both the newest CLI and its matching registry.
Reading from somewhere else
--registry accepts three shapes, and so does registry.url in the config.
bunx delacour@latest add button --registry github:acme/ui --ref mainExpands to raw.githubusercontent.com/acme/ui/<ref>/registry. A ref in the shorthand
(github:acme/ui#next) wins over --ref.
A registry is code you are about to run
Everything fetched is validated before a byte is written — a target that tries to escape its
namespace is rejected rather than followed. That guard is real, but it is not a substitute for
trusting the registry you point at.
Building your own
The registry is derived from the library's source rather than maintained beside it: one item per component folder, dependencies read off the imports, and cross-directory imports recorded as portable placeholders. There is no hand-written index to drift, and no copy of the source to keep in step.
bun --filter delacour run registry:buildThe build refuses to guess. A component folder with no metadata fails it, and so does an npm import
nobody has classified as expo install or plain — because the default would be the one that
installs a native module at a version the SDK cannot build.
Imports are resolved at build time and substituted at add time. The build does the parsing and
writes down the answer; add reads it:
"rewrites": [
{ "from": "../icon", "to": "@registry/ui/icon" },
{ "from": "../../lib/cn", "to": "@registry/lib/cn" }
]// in the library, and in the registry — the same file
import { Icon } from "../icon";
import { cn } from "../../lib/cn";
// in your project
import { Icon } from "@/components/ui/icon";
import { cn } from "@/lib/cn";That is why add needs no TypeScript parser: the parsing happened once, in the registry build,
against source we control. The builder then checks its own answer — applying the rewrites has to
reproduce the canonicalised file byte for byte, or the build fails. An import that lands in the
same directory is left alone, so a copied file reads the way the original does.
Verifying a registry
bun --filter delacour run verify:expoScaffolds a real Expo app in a temp directory, installs Uniwind and Tailwind, runs init, adds
every item, and then runs tsc --noEmit over the result. A clean typecheck is the proof that
every copied component resolves every import it makes — which no fixture can establish, because a
fixture has no dependency tree to resolve against.
It also asserts what a build alone cannot:
Prop
Type
bun --filter delacour run verify:expo --no-install # structure only, offline
bun --filter delacour run verify:expo --bundle # ...and compile it with Metro
bun --filter delacour run verify:expo --simulator # ...and render it on a device
bun --filter delacour run verify:expo --only button # one component and its closureA clean tsc is not the whole story
Uniwind compiles className in a Metro transform, so a class the scanner never saw is dropped
from the build with nothing logged — and tsc cannot see that at all. --bundle runs
expo export, which is the whole pipeline short of the native build, and is the cheapest stage
that catches it. --simulator goes one further and builds a dev client, because Reanimated,
Gesture Handler and the keyboard controller are native modules Expo Go cannot load.
CI runs the offline pass on every push and the full pass in its own job. If you maintain a fork or
your own registry, point it at yours with --registry.