@maholan/registry
v0.3.2
Published
MHL UI component registry — builds JSON served by the CLI
Readme
@maholan/registry
Component registry builder for the MHL Untitled UI Platform. Reads
registry.json (the source manifest), inlines component source files from
packages/ui, and outputs individual JSON files consumed by @maholan/cli.
What This Package Does
This package is a build-time tool — it is not published to npm and has no
runtime exports. Its sole job is to produce the public/r/*.json files that the
CLI fetches when a consumer runs npx @maholan/cli add <component>.
registry.json ← source manifest (edited by humans)
src/build.ts ← build script
│
▼ pnpm build
public/r/
├── index.json ← full registry index
├── source-map.json ← monorepo import path → owning registry item
├── button.json ← Button component (source inlined)
├── button-stories.json
└── ... ← one file per registry itemRegistry Manifest (registry.json)
The manifest is the single source of truth for what components exist and where their source files live in the monorepo.
Schema
Each item in items[] follows this shape:
{
"name": "button",
"type": "registry:component",
"title": "Button",
"description": "Accessible button built on React Aria.",
"dependencies": [
"react-aria-components",
"class-variance-authority",
"clsx",
"tailwind-merge"
],
"registryDependencies": [],
"files": [
{
"path": "registry/mhl/button/button.tsx",
"sourcePath": "../ui/src/components/base/buttons/button/button.tsx",
"type": "registry:component"
},
{
"path": "registry/mhl/button/button.variants.ts",
"sourcePath": "../ui/src/components/base/buttons/button/button.variants.ts",
"type": "registry:component"
}
],
"cssVars": {
"light": { "brand-600": "257 90% 58%" },
"dark": { "brand-600": "257 90% 65%" }
}
}Field reference
| Field | Description |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| name | kebab-case identifier — used as the CLI add argument and as the output filename (button.json) |
| type | registry:component for components, registry:file for standalone files (e.g., stories) |
| title | Human-readable display name |
| description | Plain-English description for CLI output and LLM context |
| dependencies | npm packages installed into the consumer project |
| registryDependencies | Other MHL components this one depends on — resolved recursively by the CLI |
| files[].path | Consumer-facing install path (written into the output JSON as-is) |
| files[].sourcePath | Path relative to packages/registry/ to the source file in the monorepo — read at build time and stripped from output |
| files[].target | Optional override for where the CLI writes the file in the consumer project |
| cssVars | CSS custom properties introduced by this component, for both light and dark modes |
sourcePathis a build-time field. It never appears in the built JSON files consumers download. The build script reads the file atsourcePath, inlines its contents as"content", and removessourcePathfrom the output.
Build
# From repo root
pnpm --filter @maholan/registry build
# From this package directory
pnpm buildThe build script (src/build.ts):
- Reads
registry.json - For each item, reads every
sourcePathand inlines the file content - Writes one JSON file per item to
public/r/<name>.json - Writes
public/r/index.jsonwith the full registry index (no file content)
Output Format
public/r/<name>.json (component file)
{
"name": "button",
"type": "registry:component",
"title": "Button",
"description": "...",
"dependencies": ["react-aria-components", "..."],
"registryDependencies": [],
"files": [
{
"path": "registry/mhl/button/button.tsx",
"content": "\"use client\";\n\nimport React from \"react\";\n...",
"type": "registry:component"
}
],
"cssVars": { "light": {}, "dark": {} }
}public/r/index.json (full index)
[
{
"name": "button",
"type": "registry:component",
"title": "Button",
"description": "...",
"dependencies": ["..."],
"registryDependencies": []
}
]public/r/source-map.json (cross-component import correction)
{
"@/components/base/input/hint-text": "hint-text",
"@/components/base/input/label": "label",
"@/components/base/tags/tag": "tag"
}Why this exists: some components import a sibling via the monorepo's
internal folder layout (e.g. select's source says
import { HintText } from "@/components/base/input/hint-text") even though the
target installs as its own flat, top-level component for consumers
(hint-text, not nested under input/). The only data that ties that internal
import path to the registry item which actually owns it is sourcePath — and
sourcePath is intentionally stripped from every other output file before
publishing (see the field reference above), since consumers never need it.
So this map is built once here, while sourcePath is still available, and
published as its own file. @maholan/cli's add/diff commands fetch it and
use it to rewrite each @/components/base/... import to the item's real install
path instead of naively stripping the components/base/ prefix and leaving the
rest of the (possibly stale) path untouched.
A directory is only included when exactly one component-level item
(registry:component or registry:lib) claims it — a -stories companion
sharing the same source folder doesn't count as a conflict, but two different
components sharing a folder (e.g. a shared barrel re-exporting from several
sibling items) do, and that directory is skipped as ambiguous. Anything that
imports through an ambiguous barrel should import the specific leaf file it
needs directly instead (see buildSourcePathMap in src/build.ts for the exact
rule), the same way the rest of the codebase already does.
Deployment
public/r/ is a static directory — serve it from any HTTP host. The CLI reads
the registry URL from mhl.config.json:
{
"registries": {
"@maholan": "https://mhl-ui.dev/r"
}
}The CLI fetches <registryUrl>/<name>.json for each component:
https://mhl-ui.dev/r/button.json
https://mhl-ui.dev/r/input.jsonVercel (current setup)
vercel.json in this package configures the deployment. The public/r/
directory is served at the root of the deployment.
Adding a New Component
- Create the component in
packages/ui/src/components/<name>/(all 5 files:.tsx,.variants.ts,.stories.tsx,.test.tsx,index.ts) - Add an entry to
registry.jsonwith all fields filled in - Run
pnpm --filter @maholan/registry buildto generate the JSON - Verify
public/r/<name>.jsoncontains the correct inlined source - Commit both
registry.jsonandpublic/r/<name>.json
Related Packages
@maholan/ui— Source of all component files referenced bysourcePath@maholan/cli— Fetches the built JSON files at runtime
