Build your own registry
Publish your own components as a registry that ndop installs, public or private, from any static host.
A registry is a folder of static JSON files: one per item, each with its
files' contents. ndop build writes it from your sources, any static host
serves it, and your users install from it with ndop add, next to Ndop's
own components. A registry can be public, or private behind a token.
The quickest start is the registry template: a minimal registry with one
widget item that depends on Ndop's Button, and a GitHub Actions workflow
that publishes it to GitHub Pages.
The source: registry.json
A registry's source is a registry.json that lists its items, next to
their files. Its format is
https://ndop.dev/schema/registry.json,
and each item follows
https://ndop.dev/schema/registry-item.json,
a superset of shadcn's item format:
{
"$schema": "https://ndop.dev/schema/registry.json",
"name": "acme",
"homepage": "https://ui.acme.dev",
"items": [
{
"name": "widget",
"type": "registry:ui",
"version": "0.1.0",
"title": "Widget",
"description": "A card-like panel for one piece of a dashboard.",
"registryDependencies": ["@ndop/button"],
"files": [
{
"path": "ui/widget/widget.ts",
"type": "registry:ui",
"target": "{ui}/widget/widget.ts"
}
],
"angular": {
"minVersion": "21.0.0",
"exports": ["Widget", "WidgetAction"],
"zoneless": true,
"ssr": true
},
"ndop": { "metadata": "metadata/widget.json" }
}
]
}targetsays where a file goes in the app.{ui},{lib}and{blocks}are the folders from the app'sndop.json.dependencieslists npm packages the item imports, whichndop addinstalls.registryDependencieslists items to install first. A bare name, such as"helper", is an item of the same registry;@ndop/buttonis Ndop's Button, from whichever registry the app uses for Ndop's items.versionis the item's semantic version. Bump it whenever its files change.
A registry:ui item also needs a docs.md next to its file, with an intro
and "Do" and "Don't" lists, and its TSDoc on every export. ndop build
extracts both into metadata/<name>.json, which agents read through the
Ndop MCP server. Components, blocks and themes need a
changelog/<name>.md with a ## <version> section per version.
An item's files may import Angular, the packages in its dependencies,
its own files, and through @/ the files of the items it lists in
registryDependencies, as in import { cn } from '@/lib/utils'. Those
can be items of the same registry or Ndop's: an item that lists
@ndop/button can import @/ui/button/button, at the path it has in
Ndop's registry. The template's widget does, for its action:
import { Directive } from '@angular/core';
import { Button } from '@/ui/button/button';
@Directive({
selector: 'button[sampleWidgetAction], a[sampleWidgetAction]',
hostDirectives: [{ directive: Button, inputs: ['variant', 'size', 'class'] }],
})
export class WidgetAction {}ndop build fetches the @ndop/ items an item lists, and theirs in turn,
from Ndop's registry: --registry <url> if you pass it, else the
NDOP_REGISTRY environment variable, else ndop.dev. It type-checks your
files against theirs, so install the packages their files import as dev
dependencies; the build names any that's missing. Items of other
registries, such as @acme/table, are installed with yours but can't be
imported in code.
Build it
Add the CLI and the Angular compiler, which the type-check runs, as dev dependencies, and build:
npm install --save-dev @ndop/cli @angular/core @angular/compiler @angular/compiler-cli typescript
npx @ndop/cli build --output publicAn item that imports Ndop's Button also needs what its files import, and the Angular CDK's peers, at your Angular version:
npm install --save-dev @ndop/core @ndop/primitives @angular/cdk @angular/common @angular/forms @angular/platform-browserndop build validates every item, checks its imports, type-checks it with
ngc, checks its docs, and writes the registry to public/r/:
| File | Holds |
|---|---|
r/index.json |
Every item, without file contents. |
r/<name>.json |
Each item's latest version, with its files. |
r/<name>@<version>.json |
Every released version, which updates merge from. |
r/metadata/<name>.json |
Each component's API and docs, for agents. |
r/changelog/<name>.json |
Each item's versions and what changed in each one. |
It writes nothing when a check fails. With --dry-run it only checks.
Versions and updates
The first time a version builds, ndop build writes
snapshots/<name>@<version>.json. Commit it. If a version's files change
later without a version bump, the build fails, and with --frozen-snapshots
(the default when CI is set) a version without a committed snapshot
fails too.
Those snapshots become r/<name>@<version>.json, which is how
ndop update works for your users: it fetches the version they installed
as the merge base, so their local edits merge with your changes, three-way.
ndop diff uses them to show what changed. Never delete or edit a
published version.
Host it
The output is static JSON, so any static host works: GitHub Pages,
Cloudflare Pages, Netlify, S3, or your own server. Serve public/ as it
is, with application/json for .json files. The template's workflow
builds on every push to main and deploys to GitHub Pages; in the
repository's settings, set Pages to deploy from GitHub Actions.
Install from it
Users add your registry to their ndop.json under a namespace, with a URL
template containing {name}:
{
"registries": {
"@acme": "https://acme.github.io/ui/r/{name}.json"
}
}Then install items by namespace and name:
npx @ndop/cli add @acme/widgetpnpm dlx @ndop/cli add @acme/widgetyarn dlx @ndop/cli add @acme/widgetbunx @ndop/cli add @acme/widgetndop add installs the item's dependencies too, from their own
registries: @acme/widget brings Ndop's Button with it. ndop.lock.json
records each item under its namespace, as @acme/widget, so
ndop update @acme/widget, ndop diff @acme/widget and ndop doctor
work on it as on Ndop's items.
Private registries
A private registry is the same JSON behind authentication. Users add
headers to its entry, and ${VAR} in a header comes from an environment
variable, so the token stays out of the repository:
{
"registries": {
"@acme": {
"url": "https://ui.acme.dev/r/{name}.json",
"headers": { "Authorization": "Bearer ${ACME_TOKEN}" }
}
}
}ACME_TOKEN=… npx @ndop/cli add @acme/widgetWhen the variable isn't set, installing from that registry fails with
"Registry @acme needs the ACME_TOKEN environment variable."; the app's
other registries keep working. The headers are sent only to that
registry's URL. GitHub Pages can't check a token, so host a private
registry where you can, for example behind Cloudflare Access or a small
server that checks the Authorization header.
Install by URL
Any item can also be installed from its URL, with no ndop.json entry:
npx @ndop/cli add https://acme.github.io/ui/r/widget.jsonpnpm dlx @ndop/cli add https://acme.github.io/ui/r/widget.jsonyarn dlx @ndop/cli add https://acme.github.io/ui/r/widget.jsonbunx @ndop/cli add https://acme.github.io/ui/r/widget.jsonThe lockfile records the URL, and ndop update fetches it again, merging
from <url without .json>@<version>.json, which ndop build writes. A
URL has no namespace, so bare names in its registryDependencies come
from Ndop's registry, as @ndop/ ones do; items that depend on other
items of yours are better installed by namespace. URLs must use HTTPS,
except on localhost.
Start from the template
The registry template is a folder with:
registry.json, with thewidgetitem;ui/widget/widget.tsanddocs.md, its source and docs;changelog/widget.mdandsnapshots/, its release history;package.json, whosebuildscript runsndop build --output public;.github/workflows/deploy.yml, which builds and deploys to GitHub Pages.
To make it yours, rename the registry in registry.json and set its
homepage, replace the widget with your items, run npm run build, and
commit the snapshots it writes. Ndop's install test builds the template
and installs its widget behind a token on every change, so it keeps
working with the current CLI.