This is a published format. facet add consumes it, forks emit it, and changing it incompatibly
means bumping RegistryIndex.version.
What a registry serves
<base>/index.json the index the CLI reads first<base>/<name>.json one payload per item, source text inlinedPlus, at the site root rather than under the registry base, two generated files:
| Path | |
|---|---|
schema.json | The JSON Schema every facet.json names in its own $schema |
revisions.json | Which immutable revisions exist, and which one the moving registry mirrors |
Both are generated by the deploy that publishes the registry rather than committed, so neither can lag the CLI that writes files pointing at them.
The CLI reads index.json first, and item payloads lazily for what is being added, removed or
diffed. Remote reads send accept: application/json and time out after 15 seconds.
Revisions
The published registry is served twice — once at a path that moves, and once per push at a path that never will:
r/index.json the moving registry every CLI reads by defaultr/button.jsonr/a1b2c3d/index.json the same bytes, frozen, foreverr/a1b2c3d/button.jsonA revision is keyed by the commit SHA that produced it. That is the only identifier already
immutable, unique per publish, and meaningful outside the registry. A CLI version would not work.
Three components shipped under 0.3.0 on separate pushes, because a component reaches users by
being published rather than by a CLI release.
The two copies are byte-for-byte identical rather than re-rendered, and a test asserts they match.
revisions.json at the site root lists what exists, newest first:
{ "latest": "d345608", "revisions": [ { "revision": "d345608", "generatedBy": "0.4.0", "components": 23 }, { "revision": "821604a", "generatedBy": "0.3.1", "components": 10 } ]}It is rebuilt by scanning the published tree rather than appended to, so it cannot claim a revision that is not there.
Deleting one breaks every project pinned to it, and unlike a bad npm release there is no version to move past. registry:check guards the build. The cost of getting it wrong is “fix it on the next push, and the bad revision stays”.
To read from one, point at it as a base — see pinning a revision.
index.json
{ "version": 1, "generatedBy": "0.4.0", "items": [ { "name": "button", "type": "registry:ui", "description": "Button with variant and size recipes", "files": ["ui/button.tsx"], "registryDependencies": ["utils", "text"], "dependencies": [ "@facet-ui/react-variants@^0.1.1", "@lattice-ui/react-runtime@^0.8.0" ], "tokens": ["primary", "primary-foreground", "…"] } ]}| Field | |
|---|---|
version | Registry format version. 1 today. A CLI that meets a higher one refuses and says to upgrade, rather than guessing at a format it does not know. |
generatedBy | Version of the facet-rbxts release that produced the index. Informational. |
items | Every item, with its files as bare strings. |
Item fields
| Field | Type | |
|---|---|---|
name | string | Unique across the registry. What you type after facet add. |
type | registry:ui · registry:lib · registry:hook · registry:block | Decides which facet.json alias the files land under |
description | string? | Shown by facet list |
files | see below | |
dependencies | string[]? | npm packages the source imports, as name or name@range |
devDependencies | string[]? | Same, installed as dev |
registryDependencies | string[]? | Other items — never files. Resolved transitively by add |
tokens | string[]? | Semantic theme tokens the classes name, so doctor can check them against the consumer’s Vela theme |
providers | see below | React providers the component needs above the app |
providers
It exists because one failure in the registry is not a build failure.
{ "name": "dialog", "providers": [ { "name": "PortalProvider", "package": "@lattice-ui/react-layer", "props": { "container": "player-gui" }, "reason": "Lattice reads the portal target from it, and throws when a dialog opens without one" } ]}| Field | |
|---|---|
name | The component to import and wrap with |
package | Where to import it from |
props | Prop name → a symbolic value the tool resolves, not source text |
reason | Printed to the consumer, in both the prompt and doctor’s output |
facet add offers to write it into the client entry, and
facet doctor reports when it is gone.
The expression for the local player’s PlayerGui is knowledge about roblox-ts. It belongs in the tool that reads the entry file, not in a string the registry hands over to be pasted somewhere it cannot see.
The registry declares it, not the CLI. The alternative was for the CLI to know that dialog
needs PortalProvider. That would put the knowledge in a released binary. The next layered
component would then need a CLI release to be wired correctly.
version did not move for it. loadIndex rejects an index whose version it does not
recognise. Bumping to 2 would break every installed CLI to deliver a field those CLIs would
ignore. An optional field is compatible both ways. The number is for removing a field, renaming one,
or changing what an existing one means.
Item types and where they land
| Type | facet.json alias | Default directory |
|---|---|---|
registry:ui | ui | src/shared/ui |
registry:lib | lib | src/shared/lib |
registry:hook | hooks | src/shared/hooks |
registry:block | ui | src/shared/ui |
Item payloads
<name>.json is the index entry with the file list expanded from strings into objects carrying the
source text:
{ "name": "utils", "type": "registry:lib", "files": [ { "path": "lib/utils.ts", "type": "registry:lib", "content": "export { type ClassValue as ClassName, cn } from \"@facet-ui/react-variants\";\n…" } ], "dependencies": ["@facet-ui/react-variants@^0.1.1"]}| File field | |
|---|---|
path | Path under the registry’s src, e.g. ui/button.tsx. The first segment names the alias. |
type | Per-file item type, which can differ from the item’s own |
target | Overrides the alias-derived destination entirely. Rare; blocks use it |
content | The source text, inlined |
A path’s first segment is the alias and the rest is kept, so a multi-file block stays a directory.
The index carries files as bare strings, so neither the per-file type nor a target override is visible there. The first segment is read as the type, falling back to the item’s own.
A file overriding its destination with target cannot be located from the index at all. Only facet doctor reads files that way, and it reports what it could not find.
What ~/ means in the content
Registry sources address each other with a ~/ prefix, and the first segment is an alias name:
import { TextSlot } from "~/lib/text";import { cn } from "~/lib/utils";facet add rewrites these for the consumer’s project. Textually when the alias has an import
specifier, and otherwise as a relative path from where the importing file lands. An unknown alias
segment is left alone, so it fails loudly at typecheck rather than being rewritten to something
wrong.
Relative paths in registry source are a bug. The CLI can rewrite ~/lib/utils. It cannot
rewrite ../lib/utils.
What a component may import
Anything else will not resolve once copied:
@rbxts/*@lattice-ui/*@facet-ui/react-variants~/…
Validation rules
validateRegistry() runs in registry:check, so a broken registry fails CI rather than facet add:
| Rule | |
|---|---|
Unique name | |
Known type | |
| At least one file per item | |
| No file claimed by two items | |
Every registryDependencies entry resolves | |
| One spec per npm package across the whole registry |
That last one is the non-obvious one. facet add unions dependency strings across the install
set. @lattice-ui/react-runtime in one item and @lattice-ui/react-runtime@^0.8.0 in another
survive as two entries, and both reach the package manager. A dependency carries a floor when the
source needs behaviour a specific version introduced, and then the same package carries the same
spec everywhere.
What validation cannot catch is a missing npm dependency — an import the entry never declared. That ships a file that cannot compile in a project without the package.
Specs
Dependency strings are name or name@range, split on the last @ so a scoped package’s
leading one survives. @facet-ui/theme is a name, and vela-rbxts@^0.9.0 is a name and a range.
Range comparison is deliberately not a semver implementation. It answers one question for the shapes
Facet writes, ^0.9.0, >=0.8.0 and 0.1.1. Is what is installed at least the version this needs?
Anything else is reported as unverifiable rather than guessed at.
Errors
| Message | Cause |
|---|---|
The registry at … is format version N, which this CLI does not understand | version above 1 |
Could not reach the registry at … | Network, or a wrong base |
The registry returned N for … | Non-404 HTTP failure |
The registry returned something that is not JSON at … | A host serving an HTML error page |
Component "x" was not found at … | An index that disagrees with the files beside it |
Circular registry dependency: a -> b -> a | An authoring bug; throws rather than being silently broken |