Skip to content

@tabula-css/registry

Tabula 的注册表生成器(双后端 oracle),以及被 merge、linter 和 MCP 服务器共同使用的、不依赖 Tailwind 的注册表读取器。

安装

大多数项目只会传递性地接触到 @tabula-css/registry@tabula-css/cli 依赖它来运行 tabula build,而 @tabula-css/merge 在运行时依赖它的 ./read 入口。只有当你在构建自己读取已生成的 registry.json 的自定义工具(自定义 lint 规则、脚本、编辑器扩展)时才需要直接安装它——为此,只需导入 @tabula-css/registry/read

bash
npm install @tabula-css/registry

@tabula-css/registry/generate 入口(由 CLI 内部用来生成 registry.json)会引入 Tailwind 自身的工具链;@tabula-css/registry/read 则永远不会。

概览

注册表是 Tabula 封闭词汇表的具体呈现——参见概念 § 注册表即真相。这个包以两个刻意分离的子路径导出,分别提供这个故事的两个部分:

  • @tabula-css/registry/generate 通过两个相互独立的后端之一(design-system API,或一次 PostCSS 探针样式表遍历)解析 Tailwind 自身编译后的 CSS 输出来推导 registry.json,并在 CI 中互相交叉校验,作为一致性 oracle,而不是重新实现 Tailwind 的工具类语法。
  • @tabula-css/registry/read 是其他一切消费者所导入的、不依赖 Tailwind 的读取器:@tabula-css/merge 的运行时、ESLint 与 stylelint 插件,以及 MCP 服务器。让 Tailwind 远离这一半的导入图是一条刻意设立的依赖法则——Tailwind 的 API 变动可以喧闹地破坏生成,但绝不能悄无声息地破坏强制执行

导出

@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() 加载并校验一个注册表,返回一个 RegistryReaderinput 可以是已解析的 RegistryFile 对象、一段 JSON 字符串,或指向 registry.json 的文件系统路径(路径是通过正向特征识别的——单行、不含换行、少于 4096 个字符——而不是靠“看起来不像 JSON”来判断,因此一个被截断或空的文件会得到一个 TAB-E303 诊断,而不是一个令人困惑的原始 ENOENT)。当遇到格式错误的 JSON、Schema 违规,或读取器无法理解的 registrySchemaVersion 时,它会抛出 RegistryReadErrorTAB-E302)。

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

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

validateManifest() 依据 core 的 Schema 校验一个已解析的 manifest.json(完整性根——参见概念),有效时返回 null,否则返回一段拼接后的错误字符串。

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;
}

每个注册表的消费者都使用这套带类型的访问器接口,而不是手动索引 registry.json。有几个值得特别指出:

  • conflictsOf(className) 返回在基础条件下与 className 至少共享一个规范槽位的每一个类名——预先计算好的索引,cn()resolve() 的槽位所有权逻辑都建立在其之上。
  • isAtomic(className) 报告某个类是否是一个原子重置工具类(sr-onlynot-prose 等)——这类类不占用任何槽位,也永远不会被合并所丢弃或影子解析。
  • isStale(expectedSourceHash) / isStaleAgainstManifest(manifest) 将注册表记录的 sourceHash 与一个新计算出的哈希进行比较(概念中的过期性三角),tabula doctor 和 MCP 服务器的漂移检查正是据此判断一个注册表是否需要重新构建。
  • variantProducts() / hasChain(chainPrefix, family) / hasChainClass(className) 是变体封闭性访问器(schema v3)。variantProducts() 返回已声明的产物(链前缀 → 排序后的族群列表,在一份旧版注册表上为 {});hasChain()cn() 用来判断一条变体链是否已注册的运行时成员资格门禁(当且仅当 family 出现在 variantProducts[chainPrefix] 中时为真);hasChainClass() 对由 tabula except add --chain 铸造的、精确的单类链例外回答同样的问题。这三者都经过了空原型加固,因此一个用户可控的前缀或类名('__proto__''constructor')永远不可能匹配到一个继承来的键。

registry 上的每一个映射(classesvariantscustomPropertiesexceptions 等)在内部都以 null 原型重建,因此像 reader.registry.classes['constructor'] 这样的查找不会意外解析到 Object.prototype 自身的 constructor 函数——这个读取器中的每一次字符串键查找都对自有属性是安全的。

@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() 运行来自 @tabula-css/tokens 的已解析令牌流水线,然后运行其中一个后端来产出已提交的产物(registry.jsonprofile.cssmanifest.json)。它失败即关闭:如果令牌流水线报告了错误,就不会运行任何后端,也不会产出任何内容。这正是 tabula build 内部所调用的函数。

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() 在同一份输入上运行两个后端,并断言它们生成的注册表字节级一致(规范化 JSON)——一致性 oracle 由 CI 运行,用来保证后端 A 的 design-system 路径与后端 B 的真实构建路径永远不会悄悄地产生分歧。

同样从 ./generate 导出的,还有供更底层使用的内容(主要供 @tabula-css/cli 自身的构建流水线使用):assembleRegistry(将一个已解析模型加上一次遍历后的 CSS 结果转化为三个已提交的产物)、buildProbeCss/walkProbeCss(PostCSS 探针样式表后端的构建模块),以及 BackendResult 类型。REGISTRY_SCHEMA_VERSION 也在此重新导出,以确保生成器与读取器对所使用的版本永远不会产生分歧。

参见

  • @tabula-css/tokens — 产出生成器用来生成注册表的 ResolvedModel
  • @tabula-css/merge — 依据 RegistryReader 自我配置的运行时。
  • @tabula-css/core — 该读取器据以校验的 registrySchema/manifestSchema
  • 概念 — 一条注册表类条目长什么样,以及为什么注册表即真相。

Released under the MIT License.