Custom Registry
Serve your own components through kosmesis add, the same way the public Kosmesis registry does.
A registry is just a folder of JSON files — one <name>.json per component, split under a
<styleSystem>/ subfolder. kosmesis add reads it from a local directory or an http(s):// URL;
there's no server, database, or account required. This is for an internal design system, a
client's own variant of a Kosmesis component, or trying out a change before publishing it anywhere.
The CLI has three commands for this — you never have to hand-write the inlined JSON kosmesis add
fetches, or the script that builds it:
| Command | What it does |
|---|---|
kosmesis registry init [dir] | Scaffolds a registry.json index plus one example component in [dir] (default: here). |
kosmesis registry build [dir] | Builds registry.json (or registry.<styleSystem>.json) in [dir] into dist/r/<styleSystem>/<name>.json. |
kosmesis registry add <namespace> <url> | Registers the built output (a local path or a deployed URL) under @<namespace> in a consumer project's components.json. |
Neither init nor build needs an existing components.json — they operate on the registry
you're authoring, not on a Kosmesis app. add/remove/list do need one, since that's where a
consumer project records which registries it trusts.
Quickstart: your first component, end to end
Scaffold it
npx kosmesis registry init my-registryThis writes:
my-registry/
registry.json # the index — one entry per component
src/
example-button.tsx # a real, working example componentregistry.json's one entry points at src/example-button.tsx, at "version": "1.0.0", with
@praxisjs/shared (the example's import type { Children }) as its only devDependencies entry
and empty dependencies / registryDependencies. Edit the example in place, or add more items to
registry.json plus their source files under src/ — same shape either way.
Build it
cd my-registry
kosmesis registry buildkosmesis add doesn't read registry.json directly — it fetches
<registryBase>/<styleSystem>/<name>.json, one file per component, with the source already
inlined as a string. registry build reads registry.json, inlines every referenced file's
content, and writes exactly that shape to dist/r/tailwind/example-button.json (default style
system is tailwind; pass --style-system praxisjs-css for the other one, or write both
registry.tailwind.json and registry.praxisjs-css.json to build both in one call). It fails
loudly — naming the exact component — if a files[].path doesn't exist, a
registryDependencies name isn't in the index, or a component imports something it doesn't
declare (see Dependencies),
or changed without a version bump (see Versioning).
Point a project at it
From inside any Kosmesis project (one that already ran kosmesis init):
npx kosmesis registry add acme ./path/to/my-registry/dist/r
npx kosmesis add @acme/example-buttonsrc/components/ui/example-button.tsx shows up, written straight from your local folder — no
publishing, no network. Run kosmesis registry list any time to see what's registered.
That's the whole loop. Everything below is reference material for scaling this up: serving it over HTTP, style-system-specific dependencies, cross-component dependencies, and the exact JSON shape.
kosmesis registry build reference
| Flag | Default | Meaning |
|---|---|---|
[dir] (positional) | current directory | Where to look for registry.json / registry.<styleSystem>.json, and where source files[].path entries are resolved from. |
--style-system <tailwind|praxisjs-css> | tailwind | Only applies to a bare registry.json (no style-system suffix). Ignored if registry.tailwind.json and/or registry.praxisjs-css.json exist — both get built automatically when present. |
--out <dir> | <dir>/dist/r | Where the built <styleSystem>/<name>.json files are written. |
Besides one <name>.json per component, the build writes <styleSystem>/index.json (every item,
without file content, so kosmesis outdated can check the whole registry in one request) and
updates registry-lock.json next to your index (see Versioning).
Serving it for real
Once the build output is worth sharing beyond your own machine, upload it to any static file host (S3 behind a CDN, GitHub Pages, an internal file server — anything that serves plain files over HTTP) and point teammates at the URL instead of a local path:
npx kosmesis registry add acme https://ui.acme.internal/rNothing else changes — kosmesis add @acme/example-button resolves the same way whether
registries["@acme"] is a path on disk or a URL. This repo's own docs/public/r (built by
pnpm registry:build, the same idea as kosmesis registry build but wired into this monorepo's
own scripts) is exercised as a local directory the same way before it's ever deployed to
kosmesis.praxisjs.org.
Whichever registry resolves a component is trusted at install time: its file contents are
written to disk as-is, and its dependencies are installed with your package manager
automatically. Only run kosmesis registry add (or pass --registry) for a registry you
control or trust — the same bar you'd hold an npm registry mirror to.
Managing registries
| Command | What it does |
|---|---|
kosmesis registry add <namespace> <url> | Saves <url> as @<namespace> in components.json. <namespace> can be typed with or without the leading @. |
kosmesis registry list | Prints the default registry plus every configured namespace. |
kosmesis registry remove <namespace> | Forgets a namespace. |
kosmesis add @<namespace>/<component> | Resolves <component> from that namespace instead of the project's default registry. |
kosmesis add <component> --registry <url> | One-off override of the default registry for a single add call — doesn't touch components.json and doesn't affect namespaced addresses. |
A project can mix addresses freely in one call: kosmesis add button @acme/example-button installs
button from the default registry and example-button from @acme in the same run.
Dependencies: other components, npm packages, and dev-only npm packages
If one of your components imports another one from the same registry, list it under
registryDependencies instead of copying its source in — kosmesis add resolves the full closure
and writes every dependency's files too. Add a second item to the registry.json kosmesis registry init
scaffolded, alongside example-button:
{
"name": "example-card",
"registryDependencies": ["example-button"],
"files": [{ "path": "src/example-card.tsx", "type": "registry:ui" }]
}kosmesis registry build checks this at build time — it fails loudly, naming the exact component,
if registryDependencies points at a name that isn't also in the index.
registryDependencies only resolves within the same registry/namespace — there's no cross-registry
lookup. If your component depends on something from the public Kosmesis registry, either vendor
that component's source into your own registry, or note it in your docs so consumers run a second
kosmesis add for it.
npm packages (as opposed to other registry components) go under dependencies or
devDependencies instead, and are installed automatically with the consumer's detected package
manager — dependencies ships with the consumer's app, devDependencies is for build/type-check-only
packages (type declarations, codegen tools) and gets installed with -D/--save-dev:
{
"dependencies": ["some-npm-package"],
"devDependencies": ["@types/some-npm-package"]
}kosmesis registry build reads every component's imports and fails, listing each one, when a
component imports something it doesn't declare:
- an npm package must be in
dependencies, or indevDependencieswhen every import of it isimport type(it's erased from the build, so onlytscneeds it). The four packages every PraxisJS app already has (@praxisjs/core,@praxisjs/decorators,@praxisjs/jsx,@praxisjs/runtime) are the only ones that don't need declaring. - a relative import (
./example-button) must point at one of the component's own files or a file of one of itsregistryDependencies, sincekosmesis addwrites them all into the same directory.
@/ project aliases (@/lib/utils) and node: builtins aren't checked.
If a package ends up listed as a runtime dependencies entry by one component and a
devDependencies entry by another (in the same kosmesis add call), the runtime one wins — it
needs to ship, not just type-check, so it's installed once, without -D.
Versioning
Give every item a version (semver) and bump it whenever you change the
item's files or dependencies. Projects that installed it see the new version in
kosmesis outdated, and kosmesis update shows them the diff (see
Updating Components).
{
"name": "example-button",
"version": "1.1.0",
"type": "registry:ui",
"files": [{ "path": "src/example-button.tsx", "type": "registry:ui" }]
}kosmesis registry build keeps you honest with registry-lock.json, which it writes next to your
index: each item's version plus a hash of everything a consumer receives (each file's content and
target, and its dependencies, devDependencies and registryDependencies; not title or
description). Commit it. On the next build:
- an item whose content changed but whose
versiondidn't fails the build, naming the item; - a
versionlower than the locked one fails the build; - a
versionthat isn't valid semver fails the build; - otherwise the lock is updated to the new versions and hashes.
The first build after adding versions just creates the lock. Bumping a version without changing anything is allowed (for example, to re-announce a component).
version is optional for a registry you serve yourself. An item without one builds with a
warning and is served unversioned: projects that install it see its version as unknown, never as
an update, though kosmesis update <name> still compares its content. Registries built with
older Kosmesis versions keep working the same way.
The built item also carries a hash for each file (SHA-256 of its content, line endings
normalized), which you never write by hand.
The full JSON shape
For reference, here's every field a built <styleSystem>/<name>.json can have — this is what
kosmesis registry build produces from a registry.json entry, not something you write by hand:
{
"name": "example-button",
"version": "1.1.0",
"type": "registry:ui",
"title": "Example Button",
"description": "Optional, human-readable.",
"dependencies": ["some-npm-package"],
"devDependencies": ["@types/some-npm-package"],
"registryDependencies": ["some-other-component"],
"files": [
{
"path": "src/example-button.tsx",
"type": "registry:ui",
"target": "example-button.tsx",
"hash": "sha256-3b5d5c37...",
"content": "// inlined file source"
}
]
}| Field | Meaning |
|---|---|
name | Must match the filename. What consumers type after kosmesis add (or @namespace/). index is reserved. |
version | Semver, bumped on every change. Optional: without it the item is served unversioned. |
type | registry:ui, registry:lib, registry:hook, or registry:block. |
dependencies | npm packages needed at runtime, installed automatically. |
devDependencies | npm packages only needed at build/type-check time, installed automatically with -D. |
registryDependencies | Other component names in this same registry, resolved and written first. |
files[].target | Where the file lands under the consumer's aliases.ui directory. Defaults to the basename of path. |
files[].content | The full file source, inlined as a string — this is what actually gets written to disk. |
files[].hash | sha256- plus the hex SHA-256 of content, written by the build. |