Skip to content

@tabula-css/registry

Tabula's registry generator (a two-backend oracle) and the Tailwind-free registry reader consumed by merge, the linters, and the MCP server.

Install

Most projects reach @tabula-css/registry only transitively: @tabula-css/cli depends on it to run tabula build, and @tabula-css/merge depends on its ./read entry point at runtime. Install it directly only if you are building custom tooling that reads a generated registry.json yourself (a custom lint rule, a script, an editor extension) — for that, import @tabula-css/registry/read only:

bash
npm install @tabula-css/registry

The @tabula-css/registry/generate entry point (used internally by the CLI to produce registry.json) pulls in Tailwind's own toolchain; @tabula-css/registry/read never does.

Overview

The registry is Tabula's closed vocabulary made concrete — see Concepts § the registry is ground truth. This package ships both halves of that story as two separate subpath exports, deliberately kept apart:

  • @tabula-css/registry/generate derives registry.json by parsing Tailwind's own compiled CSS output through one of two independent backends (the design-system API, or a PostCSS probe-sheet walk), cross-checked against each other in CI as a conformance oracle, rather than re-implementing Tailwind's utility grammar.
  • @tabula-css/registry/read is the Tailwind-free reader every other consumer imports: @tabula-css/merge's runtime, the ESLint and stylelint plugins, and the MCP server. Keeping Tailwind out of this half's import graph is a deliberate dependency law — a Tailwind API break can break generation loudly, but must never be able to break enforcement silently.

Exports

@tabula-css/registry/read

ts
export function readRegistry(input: string | RegistryFile, options?: ReadOptions): RegistryReader;
export interface ReadOptions {
  /** Skip schema validation — only for a registry this process just produced. Default true. */
  readonly validate?: boolean;
}
export class RegistryReadError extends Error {
  readonly diagnostics: readonly Diagnostic[];
}
export function validateManifest(manifest: unknown): string | null;
export const REGISTRY_SCHEMA_VERSION: number; // 3

readRegistry() loads and validates a registry, returning a RegistryReader. input is one of a parsed RegistryFile object, a JSON string, or a filesystem path to registry.json (a path is recognized positively — a single line, no newline, under 4096 characters — rather than by "doesn't look like JSON", so a truncated or empty file gets a TAB-E303 diagnostic instead of a confusing raw ENOENT). It throws RegistryReadError on malformed JSON, a schema violation, or a registrySchemaVersion the reader does not understand (TAB-E302).

ts
import { readRegistry } from '@tabula-css/registry/read';

const reader = readRegistry('.tabula/registry.json');
reader.has('bg-surface'); // true

validateManifest() validates a parsed manifest.json against core's schema (the integrity root — see Concepts), returning a joined error string or null when valid.

RegistryReader

ts
export class RegistryReader {
  readonly registry: RegistryFile;

  has(className: string): boolean;
  getClass(className: string): RegistryClass | undefined;
  isAtomic(className: string): boolean;
  classNames(): readonly string[];

  slotProperty(slot: number): string | undefined;
  classBySlot(slot: number): readonly string[];
  conflictsOf(className: string): readonly string[];

  exceptionOf(className: string): RegistryException | undefined;
  isException(className: string): boolean;
  exceptions(): Readonly<Record<string, RegistryException>>;

  isBanned(className: string): boolean;
  banOf(className: string): BanEntry | undefined;

  variantsTable(): Readonly<Record<string, RegistryVariant>>;
  variantProducts(): Readonly<Record<string, readonly string[]>>;
  hasChain(chainPrefix: string, family: string): boolean;
  hasChainClass(className: string): boolean;

  isStale(expectedSourceHash: string): boolean;
  isStaleAgainstManifest(manifest: ManifestFile): boolean;
}

The typed accessor surface every consumer of a registry uses instead of indexing registry.json by hand. A few worth calling out:

  • conflictsOf(className) returns every class name that shares at least one canonical slot with className under the base condition — the precomputed index cn() and resolve() build their slot-ownership logic on top of.
  • isAtomic(className) reports whether a class is an atomic reset utility (sr-only, not-prose, …) — one that keys no slots and is never dropped or shadow-resolved by the merge.
  • isStale(expectedSourceHash) / isStaleAgainstManifest(manifest) compare the registry's recorded sourceHash against a freshly computed one (the Concepts staleness triangle), which is how tabula doctor and the MCP server's drift check decide a registry needs rebuilding.
  • variantProducts() / hasChain(chainPrefix, family) / hasChainClass(className) are the variant-closure accessors (schema v3). variantProducts() returns the declared products (chain prefix → sorted family list, {} on a legacy registry); hasChain() is the runtime membership gate cn() calls to decide whether a variant chain is registered (true iff family is in variantProducts[chainPrefix]); hasChainClass() answers the same question for an exact single-class chain exception minted by tabula except add --chain. All three are null-prototype-hardened, so a user-controlled prefix or class name ('__proto__', 'constructor') can never match an inherited key.

Every map on registry (classes, variants, customProperties, exceptions, …) is rebuilt with a null prototype internally, so a lookup like reader.registry.classes['constructor'] cannot accidentally resolve to Object.prototype's own constructor function — every string-keyed lookup across this reader is own-property-safe.

@tabula-css/registry/generate

ts
export interface BuildRegistryOptions {
  readonly tokensJson: string;
  readonly configJson: string;
  readonly profileVersion?: string;
  readonly backend?: 'A' | 'B'; // default 'B', the oracle
  readonly packageVersions?: Readonly<Record<string, string>>;
}
export interface BuildRegistryResult {
  readonly ok: boolean;
  readonly diagnostics: readonly Diagnostic[];
  readonly result?: BackendResult;
  readonly model?: ResolvedModel;
}
export function buildRegistry(opts: BuildRegistryOptions): Promise<BuildRegistryResult>;

export interface ConformanceResult {
  readonly equal: boolean;
  readonly a: BackendResult;
  readonly b: BackendResult;
  readonly diff?: string; // the first differing JSON path, when they diverge
}
export function checkConformance(opts: BuildRegistryOptions): Promise<ConformanceResult>;

buildRegistry() runs the resolved-token pipeline from @tabula-css/tokens and then one backend to produce the committed artifacts (registry.json, profile.css, manifest.json). It fails closed: if the token pipeline reports an error, no backend runs and nothing is produced. This is what tabula build calls internally.

ts
import { readFileSync } from 'node:fs';
import { buildRegistry } from '@tabula-css/registry/generate';

const { ok, result, diagnostics } = await buildRegistry({
  tokensJson: readFileSync('tokens/base.tokens.json', 'utf8'),
  configJson: readFileSync('tabula.config.json', 'utf8'),
});

checkConformance() runs both backends on the same inputs and asserts their registries are byte-identical (canonical JSON) — the conformance oracle CI runs to guarantee Backend A's design-system path and Backend B's real-build path never quietly diverge.

Also exported from ./generate, for lower-level use (mainly by @tabula-css/cli's own build pipeline): assembleRegistry (turns a resolved model plus a walked CSS result into the three committed artifacts), buildProbeCss/walkProbeCss (the PostCSS probe-sheet backend's building blocks), and the BackendResult type. REGISTRY_SCHEMA_VERSION is also re-exported here so a generator and a reader never disagree about which version they're looking at.

See also

  • @tabula-css/tokens — produces the ResolvedModel the generator turns into a registry.
  • @tabula-css/merge — the runtime that configures itself from a RegistryReader.
  • @tabula-css/core — the registrySchema/manifestSchema this reader validates against.
  • Concepts — what a registry class entry looks like and why the registry is ground truth.

Released under the MIT License.