shadcn/ui Registry 完全指南:registry.json 编写、include 拆分、地址方案与 GitHub 直连
本篇基于 shadcn/ui 仓库中的 Registry 编写参考文档(registry.md)展开,系统讲解 shadcn 注册表(Registry)的两种形态、根注册表元数据、include 模块化拆分、item 字段规范、依赖地址方案与 GitHub 注册表直连机制。读完后你可以独立完成一个可被 CLI 安装的第三方注册表,理解其地址解析与构建校验的底层实现,并用官方命令完成构建、验证与安装演练。
1. 注册表的两种形态:Source Registry 与 Built Registry
在动手编写之前,必须先建立正确的心理模型。shadcn 的注册表有两种形态:
- Source registry(源码注册表):项目或仓库中手工编写的
registry.json。它可以使用include拆分,且文件路径直接指向真实源文件。 - Built registry(构建后注册表):生成给 CLI 消费者使用的 JSON 文件,通常托管在
public/r目录下。使用npx shadcn@latest build从源码注册表生成这一形态。
CLI 安装器消费的是 registry item 的载荷(payload)。源码注册表本质上是一种“从真实文件手工编写这些载荷”的方式。
一个容易忽略的事实是:registry item 并不局限于 React 组件。它可以分发组件、hooks、工具函数、设计 token、页面、配置文件、文档、规则、工作流、模板、MCP 文件以及其他项目文件。这一点在 官方根注册表 中可以得到印证——其中既有 registry:ui 类型的组件 item,也有带 registry:page、registry:file、registry:component 混合文件集且携带 registryDependencies 的 registry:block item(如 dashboard-01),文件数量达数千行,是真实的、可直接对照的编写范本。
2. 根 registry.json:元数据与 item 定义
根注册表文件应当定义注册表元数据,并提供 items 或 include 二者之一。最小示例:
{
"$schema": "https://ui.shadcn.com/schema/registry.json",
"name": "acme",
"homepage": "https://acme.com",
"items": [
{
"name": "absolute-url",
"type": "registry:lib",
"title": "Absolute URL",
"description": "A utility to turn any path into an absolute URL.",
"files": [
{
"path": "lib/absolute-url.ts",
"type": "registry:lib"
}
]
}
]
}
根注册表规则(与实现一一对应):
- 根
registry.json必须包含name和homepage; items是 registry item 定义数组;include可用于将大型源码注册表拆分为多个文件;- 被 include 的注册表文件可以省略
name和homepage。
这些规则并非文档空谈,加载器在 loader.ts 的 validateRootRegistry 中会逐项检查根文件缺少 name 或 homepage 时抛出带有明确字段名的校验错误,并提示“被 include 的注册表文件可以省略这些字段”。在 schema 层,schema.ts 中的 registryBaseSchema 也通过 refine 强制“items 与 include 至少定义其一”,而最终的 registrySchema 则要求根级 name、homepage 与 items 均为必填。若你想做 IDE 校验,仓库提供了可下载编辑的 JSON Schema 文件,见 registry-item.json(schema.ts 中的注释明确要求两处保持同步)。
2.1 item 常用字段
一个较完整的 item 定义,覆盖 npm 依赖、注册表依赖与 CSS 变量注入:
{
"name": "login-form",
"type": "registry:block",
"title": "Login Form",
"description": "A login form with email and password fields.",
"dependencies": ["zod"],
"registryDependencies": ["button", "input", "label"],
"files": [
{
"path": "blocks/login-form.tsx",
"type": "registry:block"
}
],
"cssVars": {
"light": {
"brand": "oklch(0.62 0.18 250)"
},
"dark": {
"brand": "oklch(0.72 0.16 250)"
}
}
}
关键字段说明:
name:可安装的 item 名称。它不一定是文件路径;type:registry item 类型之一,如registry:ui、registry:block、registry:lib、registry:hook、registry:file、registry:page、registry:theme、registry:style、registry:font或registry:item;files:该 item 复制或生成的源文件;dependencies:npm 运行时依赖;devDependencies:npm 开发依赖;registryDependencies:该 item 依赖的其他 registry item;cssVars、css、tailwind、envVars、docs:可选的安装期附加内容。
从源码结构看,完整可写的类型枚举比文档列举的更多:schema.ts 中的 registryItemTypeSchema 还包含 registry:component、registry:base,以及内部专用的 registry:example、registry:internal。官方注册表里的 sidebar item 就展示了 tailwind(注入 Tailwind 主题扩展)与 cssVars(light/dark 双模式变量)的真实用法。
2.2 文件路径规则
- 文件路径相对于声明该 item 的
registry.json; registry:file与registry:page类型的文件必须提供target;- 源码注册表中的文件路径禁止使用远程 URL;
- 保持源文件“可复制可粘贴”:不要引入应用私有的隐藏导入。
target 必填这一点在 schema 层是硬性约束:registryItemFileSchema 用 discriminated union 区分两类文件——registry:file/registry:page 要求 target: z.string() 必填,其他类型 target 可选。对照 apps/v4/registry.json 中 dashboard-01 的写法可以直观理解 target 的语义:registry:page 文件指定 "target": "app/dashboard/page.tsx",registry:file 指定 "target": "app/dashboard/data.json",即“源路径 → 安装后落位路径”的映射。
路径安全约束同样由加载器强制执行。loader.ts 的 validateRegistryItemFiles 会拒绝四类路径:远程 URL、绝对路径、含 .. 的父级穿越,以及解析后逃逸出 chunk 目录的路径——错误信息都会精确指出是哪个 item 的哪个文件违反了哪条规则。
3. 使用 include 拆分大型注册表
大型注册表可以用 include 保持模块化:
{
"$schema": "https://ui.shadcn.com/schema/registry.json",
"name": "acme",
"homepage": "https://acme.com",
"include": ["registry/ui/registry.json", "registry/blocks/registry.json"]
}
include 规则:
- include 路径相对于声明它的
registry.json; - include 路径必须显式指向一个
registry.json文件; - 不得使用远程 URL、绝对路径或父级穿越(
..); - item 的文件路径相对于声明该 item 的注册表文件;
- item 名称在解析后的整个注册表范围内重复会导致构建失败。
被 include 的文件示例:
{
"items": [
{
"name": "button",
"type": "registry:ui",
"files": [
{
"path": "button.tsx",
"type": "registry:ui"
}
]
}
]
}
若该文件位于 registry/ui/registry.json,则 button.tsx 从 registry/ui/button.tsx 读取,而构建后输出的 item 路径是相对于根注册表的。
从源码结构看,这条“路径重写”由 loader.ts 的 rewriteRegistryItemFilePaths 完成:加载时记录每个 item 的“来源三元组”(注册表文件、所在目录、item 下标),构建时把源路径解析为绝对路径后再 path.relative(rootDir, ...) 相对根目录重写,统一为 / 分隔。也就是说,被 include 文件中的相对路径在源文件中是“局部”的,但在构建产物中一律呈现为“全局”的,这是理解 include 语义的关键。
加载器对 include 的校验比文档规则更细,值得实现者或排错者知晓:
- 根文件命名约束:使用
include的根文件必须命名为registry.json(loader.ts); - include 路径校验:resolveIncludePath 依次拒绝远程 URL、绝对路径、父级穿越、非
registry.json结尾的 basename,并强制解析结果必须仍位于 registry 根目录内; - 深度上限:
MAX_INCLUDE_DEPTH = 32,超过会提示“展平 include 树”; - 环检测:include 链成环会报
Registry include cycle detected并打印完整调用链; - 重复 include 检测:同一个
registry.json在解析树中出现两次会直接报错,并要求“移入唯一的公共 include”; - 重名检测:validateDuplicateItems 会给出两个冲突 item 各自的来源(
registryFile items[i]),方便定位重名位置。
4. registryDependencies:是地址,不是文件路径
registryDependencies 的每一项都是 item 地址(address),不是文件路径:
{
"name": "login-form",
"type": "registry:block",
"registryDependencies": ["button", "@acme/input", "acme/ui/card#v1.2.0"],
"files": [
{
"path": "blocks/login-form.tsx",
"type": "registry:block"
}
]
}
依赖规则:
- 裸名称(如
"button")指官方 shadcn item; - 裸名称永远不指同注册表或同仓库的 item;
- 命名空间依赖使用
@namespace/item-name; - GitHub 依赖使用
owner/repo/item-name; - 需要时用
owner/repo/item-name#ref钉住 GitHub 依赖版本; - ref 不会被继承:如果
owner/repo/foo#v2依赖同仓库v2上的bar,必须显式写成owner/repo/bar#v2; - 不要使用
"./bar"这类相对依赖。
4.1 地址方案分类表
面对一个 item 字符串时,应当先做分类:
| 地址 | 方案 | 含义 |
|---|---|---|
button |
shadcn | 官方 shadcn item button |
@acme/button |
namespace | 配置好的注册表 @acme 中的 item button |
@acme/ui/button |
namespace | 配置好的注册表 @acme 中的 item ui/button |
https://example.com/r/button.json |
url | 该 URL 上的构建后 item JSON |
./button.json |
file | 磁盘上的构建后 item JSON |
acme/ui/button |
github | GitHub 仓库 acme/ui 中的 item button |
acme/ui/forms/login#main |
github | GitHub 仓库 acme/ui 中 ref main 上的 item forms/login |
对 namespace 和 GitHub 地址,带斜杠的 item 名是允许的,且它们是 item 名而非文件路径。以 .json 结尾的地址保持 file 地址优先级,因此 acme/ui/data/schema.json 会被视为文件路径而不是 GitHub item 地址。
4.2 CLI 的解析顺序
这个分类逻辑在 CLI 中有精确的纯函数实现。address.ts 的 resolveItemAddress 按固定顺序尝试四种方案:
isUrl命中 →url方案;isLocalFile命中 →file方案(这就是.json结尾优先于 GitHub 解析的原因);- parser.ts 用正则
^(@a-zA-Z0-9?)\/(.+)$匹配@namespace/item→namespace方案; resolveGitHubItemAddress匹配owner/repo/item[#ref](要求至少 3 段路径)→github方案;- 其余一律回落到
shadcn官方方案。
GitHub 段还有严格的大小写/字符约束:owner 需匹配 GITHUB_OWNER_PATTERN(字母数字与连字符,不允许连续连字符),repo 需匹配 GITHUB_REPO_PATTERN 且不得为 ./..,ref 不允许空白、控制字符或以 - 开头(address.ts)。这些细节解释了为什么一个看似合法的地址字符串会被拒绝。
5. GitHub 注册表:让公共仓库直接充当源码注册表
任何根目录带有 registry.json 的公开 GitHub 仓库都可以直接作为源码注册表被 CLI 消费。地址格式:
owner/repo/item-name[#ref]
规则:
- 前两个路径段是 GitHub owner 与 repo;
- 其余所有路径段构成 registry item 名;
- 源入口始终是根
registry.json; - GitHub 注册表是被 CLI 直接消费的源码注册表,不需要
shadcn build,也不需要生成 item JSON 文件; include遵循与本地注册表相同的源码注册表规则;- 目前仅支持
github.com上的公开仓库;私有仓库与 GitHub Enterprise 需要明确的产品决策。
5.1 为什么必须先把 ref 解析成 commit SHA
实现 GitHub 注册表拉取时,应当在读取源文件之前把 ref 解析为 commit SHA。不要直接从 raw.githubusercontent.com 读取流动的 ref,因为分支类 ref 可能被缓存数分钟,导致一次命令中读到不一致的仓库状态。推荐的解析流程:
owner/repo[#ref]
-> resolve ref with git ls-remote
-> commit SHA
-> read https://raw.githubusercontent.com/{owner}/{repo}/{sha}/registry.json
-> read includes and item files from the same SHA
这保证一条命令始终落在同一个一致的仓库快照上。完整的 40 位 commit SHA 本身稳定,可以直接使用;分支、标签与短 ref 则需要本机 Git 参与,先由 CLI 解析为 commit SHA。
github-ref.ts 正是这一流程的实现,几个可验证的细节:
- 完整 SHA 判定使用
GITHUB_SHA_PATTERN = /^[a-fA-F0-9]{40}$/(github-ref.ts),命中后直接小写返回,跳过网络解析; - 非完整 ref 通过
git ls-remote --symref解析(15 秒超时),候选优先级由 getPreferredGitHubRefNames 决定:先查refs/heads/{ref},再查refs/tags/{ref}^{}与refs/tags/{ref},最后才尝试裸 ref——即分支优先于标签; - 解析失败会给出带上下文的错误与建议(提示使用完整分支、标签或 SHA),Git 不存在时明确提示安装 Git;
- 值得注意的是,该模块还带有一条认证回退路径(
resolveGitHubRefWithAuth,配合 github-auth.ts 与 github-cli.ts),在匿名ls-remote失败时可选择鉴权解析。结合参考文档“私有仓库与 GitHub Enterprise 需要明确产品决策”的表述,可以推断这条路径是面向受限仓库预留的机制,而文档层面当前承诺的仍是公开github.com仓库。
6. 构建与验证
6.1 构建源码注册表
npx shadcn@latest build
npx shadcn@latest build registry.json --output public/r
第二条命令展示了显式指定入口文件与输出目录的用法。官方站点仓库的构建链路可参考 apps/v4/package.json 中的脚本定义:build 目标先执行 pnpm registry:build(串行构建 @shadcn/react、@shadcn/helpers、shadcn CLI 三个包,再运行 scripts/build-registry.mts 生成 public/r 下的构建产物),然后才执行 next build。
6.2 用 CLI 检查构建结果
npx shadcn@latest list @acme
npx shadcn@latest search @acme -q "login"
npx shadcn@latest view @acme/login-form
npx shadcn@latest add @acme/login-form --dry-run
npx shadcn@latest registry validate ./registry.json
对 GitHub 注册表,直接使用 owner/repo 地址即可,无需先构建:
npx shadcn@latest list owner/repo
npx shadcn@latest search owner/repo -q "login"
npx shadcn@latest view owner/repo/item
npx shadcn@latest add owner/repo/item --dry-run
npx shadcn@latest registry validate owner/repo
这套命令覆盖了“构建 → 列表 → 搜索 → 查看详情 → 干跑安装 → 校验”的完整验证闭环;--dry-run 让安装行为可在不落盘的情况下被检查,registry validate 则支持本地文件与 GitHub 地址两种目标,是发布注册表前推荐的最后一步。
7. 在 shadcn/ui 代码库中实现注册表时的守则
参考文档最后给出了面向仓库贡献者的实现守则,这些约束在上述源码中都有对应体现:
- 保持地址解析纯函数、可测试——address.ts 全文件无副作用,配套 address.test.ts 与 parser.test.ts;
- 校验器不引入副作用,loader.ts 的校验函数全部只读取与抛错;
- 保留官方 shadcn、namespace、url、file 四种既有方案的既有行为(即
resolveItemAddress的优先级不可随意调整); - 为地址解析、源码加载、依赖解析、list、search、view、add 各路径补齐测试——
packages/shadcn/src/registry/目录下几乎每个实现模块都带有同名测试文件; - 在出现多个真实 provider 之前,优先小型 source-reader 抽象,而不是插件系统。
8. 关键参考路径汇总
| 内容 | 路径 |
|---|---|
| Registry 编写参考文档(本文主体) | registry.md |
| 地址解析实现 | address.ts、parser.ts |
| include 加载、路径重写与校验 | loader.ts |
| item/注册表 Zod schema | schema.ts |
| JSON Schema(可编辑同步) | registry-item.json |
| GitHub ref 解析 | github-ref.ts |
| 官方根注册表真实示例 | apps/v4/registry.json |
| 官方构建脚本 | apps/v4/package.json |
| shadcn 技能规则(配套阅读) | SKILL.md、cli.md |
适用前提与限制:本文所述构建命令均基于 npx shadcn@latest,以当前仓库中 CLI 包(packages/shadcn)的实际实现为准;GitHub 直连注册表当前仅覆盖公开 github.com 仓库,ref 解析依赖本机 Git 且对完整 40 位 SHA 免解析;include 树深度上限 32 层、重名与重复 include 均会直接导致构建失败,编写大型注册表时应提前规划目录结构以避免踩线。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00