Facetreference

Registry format

The published JSON contract — index, item payloads, and the validation rules an authored registry has to pass.

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 inlined

Plus, at the site root rather than under the registry base, two generated files:

Path
schema.jsonThe JSON Schema every facet.json names in its own $schema
revisions.jsonWhich 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 be a version behind the CLI that writes files pointing at them.

The CLI reads index.json first and item payloads lazily, only 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 default
r/button.json
r/a1b2c3d/index.json the same bytes, frozen, forever
r/a1b2c3d/button.json

A revision is keyed by the commit SHA that produced it, because that is the only identifier already immutable, already unique per publish, and already meaningful outside the registry. A CLI version would not work: three components shipped under 0.3.0 on separate pushes, since a component reaches users by being published to the registry rather than by a CLI release.

The two copies are byte-for-byte identical, not re-rendered — an add from a pin has to produce what add produced at the time — 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. latest is the revision r/ currently mirrors.

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
versionRegistry 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.
generatedByVersion of the facet-rbxts release that produced the index. Informational.
itemsEvery item, with its files as bare strings.

Item fields

FieldType
namestringUnique across the registry. What you type after facet add.
typeregistry:ui · registry:lib · registry:hook · registry:blockDecides which facet.json alias the files land under
descriptionstring?Shown by facet list
filessee below
dependenciesstring[]?npm packages the source imports, as name or name@range
devDependenciesstring[]?Same, installed as dev
registryDependenciesstring[]?Other items — never files. Resolved transitively by add
tokensstring[]?Semantic theme tokens the classes name, so doctor can check them against the consumer’s Vela theme
providerssee belowReact providers the component needs above the app

providers

New in 0.4.0, and the reason it exists is that 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
nameThe component to import and wrap with
packageWhere to import it from
propsProp name → a symbolic value the tool resolves, not source text
reasonPrinted 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 registry declares it, not the CLI. The alternative was for the CLI to know that dialog needs PortalProvider. That knowledge would then live in a released binary, and the whole point of the hosted registry is that a component reaches users by being published rather than by a CLI release — so the next layered component would need a CLI release to be wired correctly.

version did not move for it. loadIndex rejects an index whose version it does not recognise, so bumping to 2 would break every CLI already installed in order to deliver a field those CLIs would ignore anyway. An optional field is compatible in both directions: an older CLI does not read providers, and a newer one reads undefined from an older registry. The number is for removing a field, renaming one, or changing what an existing one means.

Item types and where they land

Typefacet.json aliasDefault directory
registry:uiuisrc/shared/ui
registry:liblibsrc/shared/lib
registry:hookhookssrc/shared/hooks
registry:blockuisrc/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
pathPath under the registry’s src, e.g. ui/button.tsx. The first segment names the alias.
typePer-file item type, which can differ from the item’s own
targetOverrides the alias-derived destination entirely. Rare; blocks use it
contentThe source text, inlined

A path’s first segment is the alias and the rest is kept, so a multi-file block stays a directory rather than collapsing into the alias root.

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, otherwise as a relative path computed from where the importing file lands. An unknown alias segment is left alone deliberately, so it fails loudly at typecheck rather than being silently 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, so @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 behavior a specific version introduced, and then the same package carries the same spec in every entry.

What validation cannot catch is a missing npm dependency — an import the entry never declared. That is on the author, and it ships a file that cannot compile in a project that did not happen to have the package already.

Specs

Dependency strings are name or name@range. The CLI splits on the last @ so a scoped package’s leading one survives: @facet-ui/theme is a name, vela-rbxts@^0.9.0 is a name and a range, @lattice-ui/react-runtime@^0.8.0 is both at once.

Range comparison is deliberately not a semver implementation. It answers one question — is what is installed at least the version this needs — for the shapes Facet writes (^0.9.0, >=0.8.0, 0.1.1), and reports anything else as unverifiable rather than guessing.

Errors

MessageCause
The registry at … is format version N, which this CLI does not understandversion 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 -> aAn authoring bug; throws rather than being silently broken