@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:
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
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; // 3readRegistry() 加载并校验一个注册表,返回一个 RegistryReader。input 可以是已解析的 RegistryFile 对象、一段 JSON 字符串,或指向 registry.json 的文件系统路径(路径是通过正向特征识别的——单行、不含换行、少于 4096 个字符——而不是靠“看起来不像 JSON”来判断,因此一个被截断或空的文件会得到一个 TAB-E303 诊断,而不是一个令人困惑的原始 ENOENT)。当遇到格式错误的 JSON、Schema 违规,或读取器无法理解的 registrySchemaVersion 时,它会抛出 RegistryReadError(TAB-E302)。
import { readRegistry } from '@tabula-css/registry/read';
const reader = readRegistry('.tabula/registry.json');
reader.has('bg-surface'); // truevalidateManifest() 依据 core 的 Schema 校验一个已解析的 manifest.json(完整性根——参见概念),有效时返回 null,否则返回一段拼接后的错误字符串。
RegistryReader
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-only、not-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 上的每一个映射(classes、variants、customProperties、exceptions 等)在内部都以 null 原型重建,因此像 reader.registry.classes['constructor'] 这样的查找不会意外解析到 Object.prototype 自身的 constructor 函数——这个读取器中的每一次字符串键查找都对自有属性是安全的。
@tabula-css/registry/generate
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.json、profile.css、manifest.json)。它失败即关闭:如果令牌流水线报告了错误,就不会运行任何后端,也不会产出任何内容。这正是 tabula build 内部所调用的函数。
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。- 概念 — 一条注册表类条目长什么样,以及为什么注册表即真相。