feat: vibe-registry M1 — 万物皆注册框架骨架

- core: registerLayout/Widget/Action(泛型)/Schema + 校验闸门
- definePage: 页面只管业务,结构来自注册表
- presets: admin / app-shell 预设布局(vr add 物化)
- cli: vr list/add/update 防覆盖
- examples/qa-orchestrator: 实战编排验证(workspace:* 依赖)
This commit is contained in:
hellow-da-sb
2026-09-03 00:48:10 +08:00
commit 8c93bde5f6
30 changed files with 1289 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
# 依赖
node_modules/
.pnpm-store/
# 构建产物
dist/
build/
# 日志与系统
*.log
.DS_Store
Thumbs.db
# 编辑器
.vscode/
.idea/
# 本地临时
*.tmp
+54
View File
@@ -0,0 +1,54 @@
# vibe-registry
> 万物皆注册 · 面向 AI 前端的注册式页面框架(M1 概念验证)
## 设计
- 完整设计文档见 [`docs/design.md`](docs/design.md)(v0.5)
- 一句话:页面 = 注册项的组合;用户/agent 只需说"用 xx 布局",结构全部来自注册表
## 当前结构(M1)
```
src/registry.ts 万物皆注册的类型基石 + 全局注册表 + 校验(含泛型 action)
src/page.ts definePage 声明层(页面只管业务)
presets/layouts/ 包内预设布局源(vr add 数据源,带 demo 预览)
cli/vr.ts vr list / add / update(防覆盖)
scripts/smoke.ts 校验闸门 + id 唯一性验证
registry/layouts/ 物化后的资产空间(预设 = 你的源码)
examples/
qa-orchestrator/ 实战编排:测试编排系统(多页面 + 自定义注册 + workspace 依赖)
```
## 实战编排示例(qa-orchestrator)
验证框架真实可用性:workspace 依赖引入、自定义注册(非预设)、多页面 `definePage` 编排、`setup` 业务数据流。
```bash
cd examples/qa-orchestrator
pnpm install # workspace:* 链接根源码
pnpm typecheck # 类型检查
pnpm orchestrate # 注册清单 + 3 页面解析树 + setup 数据流验证
```
## 快速上手
```bash
pnpm install
pnpm typecheck # 类型检查
pnpm vr list # 列出可用预设布局
pnpm vr add admin # 物化预设到 registry/layouts/admin.ts(防覆盖)
pnpm vr update admin # 对比新版,预览 diff(不写文件,除非 --force)
node --experimental-strip-types scripts/smoke.ts # 校验闸门冒烟
```
## 防覆盖原则
- 目标已存在且与预设**不同**(被用户改过)→ 拒绝,绝不静默覆盖
- 内容相同 → already up-to-date
- 显式 `--force` 才允许覆盖
## 关键理念速记
1. TS 优先 · 2. 极薄渲染内核 · 3. 布局带简易 id + live 预览
4. 开放可扩展 · 5. 未使用不进产物 · 6. P 层选择预览 · 7. 预设物化进项目
+146
View File
@@ -0,0 +1,146 @@
#!/usr/bin/env node
/**
* vr —— vibe-registry CLI(M1)
*
* 用法:
* vr list 列出包内可用预设布局
* vr add <layout> [--force] 物化预设到项目 registry/layouts/(防覆盖)
* vr update <layout> [--force] 对比包内新版,拉 diff(防覆盖,不静默改你的代码)
*
* 防覆盖原则(v0.5 决策):
* - 目标文件已存在且与预设不同(被用户改过)→ 拒绝,绝不静默覆盖
* - 内容相同 → 提示 already up-to-date
* - 显式 --force 才允许覆盖(用户自己确认)
*/
import { readdirSync, readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, join, relative } from 'node:path'
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
const presetsDir = join(root, 'presets', 'layouts')
const targetDir = join(process.cwd(), 'registry', 'layouts')
function listPresets(): string[] {
return readdirSync(presetsDir)
.filter((f) => f.endsWith('.ts'))
.map((f) => f.replace(/\.ts$/, ''))
.sort()
}
function presetFile(id: string): string | undefined {
const f = join(presetsDir, `${id}.ts`)
return existsSync(f) && statSync(f).isFile() ? f : undefined
}
function targetFile(id: string): string {
return join(targetDir, `${id}.ts`)
}
function showDiff(target: string, preset: string, id: string): void {
const tLines = target.split(/\r?\n/)
const pLines = preset.split(/\r?\n/)
const max = Math.max(tLines.length, pLines.length)
const diffs: string[] = []
for (let i = 0; i < max; i++) {
const a = tLines[i] ?? ''
const b = pLines[i] ?? ''
if (a !== b) diffs.push(` L${i + 1}: 你的 "${a.slice(0, 60)}" → 新版 "${b.slice(0, 60)}"`)
if (diffs.length >= 8) break
}
console.log(` 差异预览(最多 8 行,${id}.ts):`)
diffs.forEach((d) => console.log(d))
}
function cmdList(): void {
const presets = listPresets()
console.log(`可用预设布局(${presets.length}):`)
presets.forEach((id) => {
const f = presetFile(id)!
const head = readFileSync(f, 'utf8').split(/\r?\n/).slice(0, 3).join(' ')
console.log(` - ${id}`)
console.log(` ${head.replace(/^\/\*\*?\s?/, '').slice(0, 80)}`)
})
}
function cmdAdd(id: string, force: boolean): void {
const src = presetFile(id)
if (!src) {
console.error(`✗ 未知预设布局:'${id}'`)
console.error(` 可用:${listPresets().join(', ')}`)
process.exit(1)
}
const content = readFileSync(src, 'utf8')
const dest = targetFile(id)
if (existsSync(dest)) {
const existing = readFileSync(dest, 'utf8')
if (existing === content) {
console.log(`• '${id}' 已物化且与预设一致(already up-to-date)`)
return
}
if (!force) {
console.error(`✗ 防覆盖:registry/layouts/${id}.ts 已存在且被修改过`)
console.error(` 不静默覆盖。选项:`)
console.error(` 1) 看差异:vr update ${id} (仅预览,不写文件)`)
console.error(` 2) 确认覆盖:vr add ${id} --force`)
process.exit(2)
}
console.log(`⚠ 强制覆盖 registry/layouts/${id}.ts`)
}
mkdirSync(targetDir, { recursive: true })
writeFileSync(dest, content, 'utf8')
console.log(`✓ 已物化预设 '${id}' → ${relative(process.cwd(), dest)}`)
console.log(` 从此它是你的源码,可直接修改;升级用 vr update ${id}`)
}
function cmdUpdate(id: string, force: boolean): void {
const src = presetFile(id)
if (!src) {
console.error(`✗ 未知预设布局:'${id}'`)
process.exit(1)
}
const dest = targetFile(id)
if (!existsSync(dest)) {
console.log(`• '${id}' 尚未物化,执行 vr add ${id}`)
return
}
const existing = readFileSync(dest, 'utf8')
const preset = readFileSync(src, 'utf8')
if (existing === preset) {
console.log(`• '${id}' 已是最新版`)
return
}
showDiff(existing, preset, id)
if (force) {
writeFileSync(dest, preset, 'utf8')
console.log(`✓ 已更新 ${id}(--force)`)
} else {
console.log(` 未写文件。确认升级请:vr update ${id} --force`)
}
}
const [cmd, arg, ...rest] = process.argv.slice(2)
const force = rest.includes('--force') || arg === '--force'
switch (cmd) {
case 'list':
cmdList()
break
case 'add':
if (!arg || arg === '--force') {
console.error('用法:vr add <layout> [--force]')
process.exit(1)
}
cmdAdd(arg, force)
break
case 'update':
if (!arg || arg === '--force') {
console.error('用法:vr update <layout> [--force]')
process.exit(1)
}
cmdUpdate(arg, force)
break
default:
console.error('用法:vr list | vr add <layout> [--force] | vr update <layout> [--force]')
process.exit(1)
}
+262
View File
@@ -0,0 +1,262 @@
# vibe-registry — 面向 AI 前端的"万物皆注册"页面框架(设计文档 v0.5)
> 状态:探究中(未定稿) · 作者:dsh-agent 与用户协作
> 定位:vibe coding 时代,把"AI 前端生成"从自由发挥收束到**受控注册 + 声明式组合**
---
## 1. 问题陈述
### 1.1 痛点的三张面孔
| 痛点 | 现象 | 根因 |
|---|---|---|
| **重复造轮子** | 让 AI 做个表单/列表/弹窗,它每次从零拼一遍 | AI 无"既有资产"记忆,不查你已有组件 |
| **布局不统一** | 页面间距、栅格、导航、按钮位置随生成批次漂移 | 布局规范只存在于人脑/文档,AI 不会遵守 |
| **无法收敛** | 改一处样式要动 N 个文件,AI 改完这处崩那处 | 约定不成为代码,就没有强制力 |
### 1.2 为什么组件库没解决
组件库(shadcn/antd)解决了"有组件可用",但没解决两件事:
1. **AI 不会主动用**——提示词劝说无效,生成时照样自创;
2. **页面级结构(布局/区块/页面骨架)没有注册位**——组件库只管原子,不管"一个后台页长什么样"。
### 1.3 一句话目标
> 让**页面 = 注册项的组合**,让 AI(和人)通过**受控的注册/声明语法**产出页面,
> 而不是通过自由的 JSX——从生成源头锁死重复与漂移。
---
## 2. 核心理念:万物皆注册(Everything is Registered)
### 2.1 三个公理
1. **一切可复用之物必须注册**:布局、区块、组件、操作、路由、表单字段、数据源……先注册,才可用。
2. **页面是声明的产物,不是手写的产物**:页面文件只声明"由哪些注册项、以什么配置组合",结构由注册表驱动渲染。
3. **改动即配置,配置即代码**:任何视觉/结构改动落在注册项配置里,版本可控、可 diff、可被 AI 精确编辑。
### 2.2 注册项类型(草案)
```ts
type RegistryItem =
| { kind: 'layout'; id: string; ...; preview: { demo?: PageDemo; thumbs?: string } } // 页面骨架,带 live 预览数据
| { kind: 'block'; id: string; ... } // 区块:导航/侧栏/页脚/内容容器
| { kind: 'widget'; id: string; ...; preview?: WidgetDemo } // 业务组件:表单/表格/弹窗(映射到 UI 库)
| { kind: 'action'; id: string; ... } // 行为:提交/跳转/确认(可复用事件语义)
| { kind: 'schema'; id: string; ... } // 数据/字段注册:表单字段、表格列
| { kind: 'theme'; id: string; ... } // 设计 token:间距/圆角/色板(轻量覆盖)
```
关键:
- **widget 允许指向任意第三方组件并注册**,万物皆注册的"物"是开放的;
- **预览数据是注册项的一部分**:layout/widget 注册时自带 demo 数据,选择层渲染真实形态,
预览与实装共用同一渲染内核——**预览即真实,无 mock 漂移**;
- 二次开发定制页面 = 新增 widget/layout 注册并挂入 P 层,天然获得风格/主题一体化(共用 theme + 渲染内核)。
---
## 3. 架构分层
```
┌─────────────────────────────────────────────┐
│ P. 选择/预览层(新,用户长时间停留的地方) │
│ 布局画廊:预设布局 live 预览 + 缩略图 │
│ IDE 联动(Webview/面板):选中→回写声明 │
│ 用户只做:挑布局 → 声明业务 → 生成 │
├─────────────────────────────────────────────┤
│ A. 声明层(用户/AI 产出) │
│ page.tsx 声明:layout id + blocks + │
│ 配置 + 业务回调 │
├─────────────────────────────────────────────┤
│ B. 注册表层(单一事实来源) │
│ registry/**/*.ts:注册项定义与配置 │
│ (类型安全:TS 推导注册项类型) │
├─────────────────────────────────────────────┤
│ C. 极薄 schema 渲染内核 │
│ 声明 → 按 id 查注册项 → 布局槽位填充 → │
│ widget 装配 → 输出组件树 │
├─────────────────────────────────────────────┤
│ D. 业务接入层(页面"只管业务") │
│ 页面文件只写:数据获取 / 状态 / 事件回调, │
│ 结构全部来自 C 层输出 │
└─────────────────────────────────────────────┘
```
### 3.0 选择/预览层(P 层)定位
- **拿包即画廊**:脚手架创建项目时把一批预设布局(源码)放进 `registry/**` 并生成画廊首页——`npm run dev` 打开即见(类 Vite 欢迎首页);
- **默认预设全部带 live 预览**:每个预设布局(admin/dashboard/landing…)内置一份"demo 数据渲染",用户一眼看到成品形态再决定;
- **配合 IDE 联动**(M3 增强):VS Code/Cursor Webview 面板或 HMR 预览路由——选布局、声明业务、实时看效果,**长时间停留在"选择"而不写布局**;
- **声明业务即可**:P 层把用户的"业务描述"折成 `definePage` 的 setup/blocks 声明;
- **预设与自定义一体**:项目 `registry/**` 是唯一资产空间,`vr add` 物化预设、直接改源码,二次开发定制页天然与预设同一渲染内核、风格/主题/布局统一。
### 3.1 页面文件长什么样(愿景示例)
```tsx
// 页面只管业务——不写任何布局 JSX
export default definePage({
layout: 'app-shell', // ← 注册的布局
blocks: [
{ widget: 'page-header', props: { title: '订单管理' } },
{ widget: 'filter-bar', bind: { filters } },
{ widget: 'data-table', bind: { columns, rows, onPage } },
],
// 业务:
async setup(ctx) {
const rows = await api.listOrders(ctx.query)
return { rows }
},
actions: { onPage: async (p) => { ... } },
})
```
AI 生成的"页面代码"被限制成这种形状:**结构来自注册表,AI 只填业务与配置**。
---
## 4. 面向 AI 的受控生成(AI Gate 草案)
这是与所有"低代码引擎"的分水岭——**用户是 AI,不是拖拽者**。
### 4.1 交互协议:用户只须说"xx 布局"
```
用户: "订单列表页,用 admin 布局,带筛选和分页表格"
agent: layout: 'admin' ← 唯一 id 契约
blocks: [ filter-bar, data-table ] ← 全部来自注册表
setup() { 业务 } ← 只填业务
```
- 用户**只需报布局 id**(`admin`/`dashboard`/`landing`…),布局的栅格、骨架、样式细节全在注册项里,用户与 agent 都不需要知道实现;
- 所有可用的 id 是**公开面**,agent 通过注册表目录/README 获取,不许自创 id;
- 布局 id 是**稳定契约**:实现可换,id 不变(防止 agent 记错实现造成静默漂移)。
### 4.2 受控语法与校验闸门
- **受控语法**:AI 只能产出 `definePage/registerItem` 形状的代码;
- **校验闸门**:生成产物过 `validate()`——引用了未注册的 widget/layout → 报错退回,提示"请先注册或改用已有项";
- **注册优先**:AI 想造新组件,先走"注册申请",注册完再引用;
- **收敛目标**:同一仓库内,所有 AI 生成的页面共享同一注册表 → 布局天然统一。
### 4.3 扩展:万物皆可注册(注册是开放集)
- 布局、widget、schema、action 均可由用户/agent 新增注册;
- 新增注册 = 写一个 `registerXxx({...})` 文件放 `registry/**`,自动纳入类型推导;
- **未使用不进编译和源码**:新注册但没页面引用 → 留在注册源、不进产物(零成本扩展);
- 首次被引用时自动物化,进入编译与源码树。
---
## 5. 与现有方案的分界(避免"又一个低代码")
| 方案 | 我们不做 | 我们做 |
|---|---|---|
| 低代码平台(拖拽/配置后台) | 不做可视化画布、不做运行时编辑器 | 配置即代码,存 git,可 review |
| 组件库(shadcn/antd) | 不重新造原子组件 | 组件**注册化包装**,统一暴露 |
| 代码生成器(v0/bolt) | 不只"生成页面",不解决复用 | 生成受控产物,强制复用注册项 |
| headless 框架 | 不做渲染运行时新轮子 | 渲染内核极薄,组合已有库 |
一句话:**我们不发明 UI,只发明"约束 AI 的注册协议"。**
---
## 6. 已定决策(v0.5,用户拍板)
1. **声明格式:TS 优先**——`definePage`/`registerItem` 全 TS,类型推导强、AI 好生成;YAML 仅备用于纯数据资产。
2. **渲染内核:极薄 schema 渲染器**——注册项 → 映射已有组件(antd/shadcn),内核保持 <500 行,不重造 UI。
3. **预设布局模板:内置多个、均带简易 id 与 live 预览**——如 `app-shell` / `admin` / `dashboard` / `landing` / `form-page`……用户(或用户告诉 agent)**只需说"用 xx 布局"**,agent 按 id 取布局,无需理解布局实现;每个布局附带 demo 预览,选择层直接可看。
4. **可扩展**:用户可注册自己的布局/widget(文件约定,见 §8),注册是开放集;二次开发定制页可并入现有布局,与常见组件库联动,风格/主题一体化。
5. **未使用不进编译、不进源码**——只有被页面**实际引用**的注册项才进入编译产物与源码树;未被引用的注册项不物化、不打包。
6. **选择/预览层(P 层)**:用户长时间停留在"选布局 + 声明业务",由 P 层回写声明文件。
7. **预设物化进项目 + 拿包即画廊(类 Vite 首页)**——预设与自定义**物理一体**,消灭两套资产:
- **预设不是黑盒包**:CLI `vr add <layout>` 或脚手架创建时,把预设布局**以源码拷贝进**项目 `registry/**`(类 shadcn add + Vite 模板心智);
- 之后预设与自定义**同目录、同源码、同改法**——想改布局细节直接改源码,无"包内预设不可改";
- **升级**:框架发新版,`vr update` 拉 diff,用户选择是否同步(类 shadcn upgrade);
- **画廊**:项目 `registry/**` 自带 demo 数据,dev 首页列出全部已注册项 live 预览,选中即生成 `definePage` 声明;
- **未使用不进产物**:物化进项目的预设,仍只有被页面**引用**才进入编译产物。
### 6.1 注册与物化的统一模型(决策 5 × 7)
```
包内预设 registry 源 ──vr add / 脚手架──> 拷贝进项目 registry/**(与自定义同源码)
│
项目 registry/**(唯一资产空间)
│ 被页面引用 → 物化进产物
▼
编译产物 / 源码树(仅物化项)
```
- **一个资产空间**:项目 `registry/**` 是唯一事实来源,预设与自定义无差别;
- **物化 = 被引用**:bundler 树摇保证未引用注册项不打包;
- **画廊 = dev 首页**:渲染项目内全部已注册项 live 预览,生产不含画廊。
### 6.1 决策 5 的机制:按需物化(On-Demand Materialization)
```
注册表(全量,dev 可见) ──静态引用分析──> 物化清单(Materialized Set)
│ 只装载被引用项
▼
编译产物 / 源码树(仅物化项)
```
- 触发时机:**生成期**(定义页面的构建/生成脚本时)做全量静态扫描,收集所有 `definePage` 中被引用的 layout/widget/schema id;
- 未被引用的注册项停留在"注册源"里,**不生成运行时文件、不进 bundle**;
- 好处:产物极轻、无死代码、新增预设布局零成本(不用就不存在);
- 张力与对策:见 §6.2。
### 6.2 新决策暴露的两个张力(需留意,不阻塞)
1. **"未使用即不物化" vs "注册即复用"**:一个 widget 被页面 A 注册、但尚未被任何页面引用——它算"未使用"而被裁剪吗?
→ 对策:物化判断以**引用**为准;仅注册未引用 = 留在注册源但不进产物(符合你的设计);
"想让它先进产物"需显式 `include`(用于模板扩展场景)。
2. **布局 id 语义漂移**:agent 记忆里的 `admin` 和你注册的 `admin` 若实现不同,会静默用错布局。
→ 对策:布局 id 是**稳定契约**,实现可换、id 不变;id 表是文档化的公开面,进 README 供 agent 引用。
---
## 8. 扩展注册机制(文件约定)
```
registry/ ← 唯一资产空间(预设 + 自定义同在此)
layouts/ admin.ts registerLayout('admin', { skeleton: [...] })
app-shell.ts ← vr add app-shell 从包内拷贝而来
widgets/ table.ts registerWidget('data-table', { render: Table, schema })
filter.ts ...
schemas/ order.ts registerSchema('order-fields', ...)
actions/ submit.ts registerAction('submit-order', ...)
```
- 每个文件幂等注册一个 id;重名 id = 编译报错(唯一性强制);
- 目录/文件路径仅组织用,**id 才是引用键**(满足"简易 id"诉求);
- **预设 = `vr add` 拷贝进此目录的源码**,与自定义无差别,可直接修改;
- `vr update` 对比包内新预设 diff,用户选择同步(可保留本地改动);
- 未引用的注册文件可留在仓库作为资产,但不出产物(决策 5)。
## 9. MVP 范围(建议第一刀)
**阶段 M1:类型 + 渲染内核 + 静态物化(不接 UI 库)**
- [ ] `registerItem()` + 目录式注册表,类型推导
- [ ] `definePage()` + 布局槽位填充(首内置:`admin`/`app-shell` 布局示例)
- [ ] 注册项带 demo 预览数据的类型骨架(为 P 层铺路)
- [ ] `validate()` 校验闸门(未注册引用即报错)
- [ ] 静态引用分析:未使用注册项不进产物(决策 5 的最小实现)
**阶段 M2:接入真实 UI 库 + 引入即预览**
- [ ] widget 映射 antd/shadcn(首批 8-10 个:按钮/表格/表单/弹窗/筛选条/分页…)
- [ ] 引入即注册验证:import 注册模块 → 自动进入画廊
- [ ] dev 首页(布局画廊):缩略图 + live 预览 + demo 数据(类 Vite 首页)
- [ ] 2-3 个示例后台页,证明"同一注册表,AI 生成即统一"
**阶段 M3:生成回写 + IDE 联动**
- [ ] 画廊选中布局 → 声明业务 → 生成 `definePage` → 回写文件
- [ ] VS Code/Cursor Webview 或 HMR 面板接入
## 10. 命名候选(未定)
- `vibe-registry` / `regui` / `page-registry` / 中文语境:万物注册?敬请给方向
+18
View File
@@ -0,0 +1,18 @@
{
"name": "qa-orchestrator",
"version": "0.0.1",
"private": true,
"type": "module",
"description": "vibe-registry 实战编排:测试编排系统(多页面 + 自定义注册)",
"scripts": {
"typecheck": "tsc --noEmit",
"orchestrate": "node --experimental-strip-types scripts/orchestrate.ts"
},
"dependencies": {
"vibe-registry": "workspace:*"
},
"devDependencies": {
"typescript": "^5.8.0",
"@types/node": "^22.0.0"
}
}
@@ -0,0 +1,37 @@
/**
* 套件总览页(overview)
* 业务:QA 看板上展示所有套件、总通过率、失败数
* 布局:qa-console;页面只管业务数据
*/
import { definePage } from 'vibe-registry'
export const overviewPage = definePage({
layout: 'qa-console',
title: '测试编排 · 总览',
blocks: [
{ widget: 'suite-list', bind: { suites: 'suites' } },
{
widget: 'suite-detail-view',
props: { mode: 'summary' },
bind: { cases: 'summary' },
},
],
setup: async () => {
// 真实场景:从 API 拉取;这里用模拟数据演示编排
const suites = [
{ id: 'auth', name: '认证模块', passedRate: 0.92, lastResult: 'passed' },
{ id: 'billing', name: '计费模块', passedRate: 0.71, lastResult: 'failed' },
{ id: 'search', name: '搜索模块', passedRate: 1.0, lastResult: 'passed' },
]
const total = suites.length
const failed = suites.filter((s) => s.lastResult === 'failed').length
return {
suites,
summary: {
total,
failed,
passingRate: Math.round((1 - failed / total) * 100),
},
}
},
})
@@ -0,0 +1,27 @@
/**
* 运行记录页(run history)
* 业务:全部历史运行 + 失败率统计
*/
import { definePage } from 'vibe-registry'
export const runHistoryPage = definePage({
layout: 'qa-console',
title: '测试编排 · 运行记录',
blocks: [
{ widget: 'suite-list', bind: { suites: 'suites' } },
{ widget: 'suite-detail-view', props: { mode: 'history' }, bind: { cases: 'runs' } },
],
setup: async () => {
return {
suites: [
{ id: 'auth', name: '认证模块', passedRate: 0.92, lastResult: 'passed' },
{ id: 'billing', name: '计费模块', passedRate: 0.71, lastResult: 'failed' },
],
runs: [
{ id: 'r1', name: 'auth #0421', priority: 'P0', status: 'passed', durationMs: 980 },
{ id: 'r2', name: 'auth #0420', priority: 'P1', status: 'skipped', durationMs: null },
{ id: 'r3', name: 'billing #0419', priority: 'P0', status: 'failed', durationMs: 2100 },
],
}
},
})
@@ -0,0 +1,42 @@
/**
* 套件详情页(suite detail)
* 业务:展示单个套件的用例列表,提供重跑/停止动作
* 注意:页面不内联任何"重跑逻辑",只声明引用 run-suite action —— 行为万物皆注册
*/
import { definePage } from 'vibe-registry'
export const suiteDetailPage = definePage({
layout: 'qa-console',
title: '测试编排 · 套件详情',
blocks: [
{ widget: 'suite-list', bind: { suites: 'suites' } },
{
widget: 'suite-detail-view',
props: { mode: 'detail', suiteId: 'current' },
bind: { cases: 'cases', suite: 'suite' },
},
],
setup: async (ctx) => {
const suiteId = ctx.params.id ?? 'auth'
const cases = [
{ id: 'a1', name: '登录成功', priority: 'P0', status: 'passed', durationMs: 120 },
{ id: 'a2', name: '登录失败锁定', priority: 'P1', status: 'failed', durationMs: 340 },
{ id: 'a3', name: '令牌过期刷新', priority: 'P1', status: 'running', durationMs: null },
]
return {
suites: [
{ id: 'auth', name: '认证模块', passedRate: 0.92, lastResult: 'passed' },
{ id: 'billing', name: '计费模块', passedRate: 0.71, lastResult: 'failed' },
],
suite: { id: suiteId, name: `${suiteId} 套件` },
cases,
}
},
actions: {
rerun: async (payload: { suiteId?: string }) => {
// 页面只做"把用户意图转成 action 调用",实现仍在注册表
const { runSuiteAction } = await import('../registry/actions/run-suite.ts')
return runSuiteAction.handler({ suiteId: payload.suiteId ?? 'auth' }, null)
},
},
})
+48
View File
@@ -0,0 +1,48 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
vibe-registry:
specifier: file:../..
version: file:../..
devDependencies:
'@types/node':
specifier: ^22.0.0
version: 22.20.1
typescript:
specifier: ^5.8.0
version: 5.9.3
packages:
'@types/node@22.20.1':
resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
vibe-registry@file:../..:
resolution: {directory: ../.., type: directory}
snapshots:
'@types/node@22.20.1':
dependencies:
undici-types: 6.21.0
typescript@5.9.3: {}
undici-types@6.21.0: {}
vibe-registry@file:../..: {}
@@ -0,0 +1,15 @@
/**
* run-suite —— 重跑测试套件 action
* 行为注册:页面的"重跑"按钮只声明引用该 action,不内联逻辑
*/
import { registerAction } from 'vibe-registry'
export const runSuiteAction = registerAction({
kind: 'action',
id: 'run-suite',
description: '重跑指定测试套件(全量)',
handler: async (payload: { suiteId: string }, _ctx) => {
// M1:真实执行器后续接入;这里返回执行意图供编排层消费
return { started: true, suiteId: payload.suiteId, at: Date.now() }
},
})
@@ -0,0 +1,13 @@
/**
* stop-run —— 停止运行中的套件 action
*/
import { registerAction } from 'vibe-registry'
export const stopRunAction = registerAction({
kind: 'action',
id: 'stop-run',
description: '停止正在运行的测试套件',
handler: async (payload: { runId: string }, _ctx) => {
return { stopped: true, runId: payload.runId, at: Date.now() }
},
})
@@ -0,0 +1,16 @@
/**
* 资产集中注册入口
* 模拟脚手架/自动化生成:所有 registry/** 在此汇聚(副作用 import)
* 页面只需 import 本文件即可让全部注册生效
*/
import './layouts/qa-console.ts'
import './widgets/suite-list.ts'
import './widgets/suite-detail-view.ts'
import './widgets/status-badge.ts'
import './schemas/test-case-fields.ts'
import './actions/run-suite.ts'
import './actions/stop-run.ts'
// 注册完成后,导出注册表供编排层校验
import { registry } from 'vibe-registry'
export { registry }
@@ -0,0 +1,19 @@
/**
* qa-console —— 测试编排控制台布局(自定义布局,非预设)
* 业务场景:QA 负责人在侧栏切换套件,主区查看运行结果
*/
import { registerLayout } from 'vibe-registry'
export const qaConsoleLayout = registerLayout({
kind: 'layout',
id: 'qa-console',
slots: ['suite-sidebar', 'main-content'],
description: '测试编排控制台:左侧套件列表 + 主内容区',
preview: {
title: 'QA Console · 测试编排控制台',
demoBlocks: [
{ slot: 'suite-sidebar', widget: 'suite-list', props: { suites: 'suiteSummary' } },
{ slot: 'main-content', widget: 'suite-detail-view' },
],
},
})
@@ -0,0 +1,17 @@
/**
* test-case-fields —— 测试用例字段 schema
* 用例表格/表单的列定义来源(万物皆注册:连列都是注册的)
*/
import { registerSchema } from 'vibe-registry'
export const testCaseFields = registerSchema({
kind: 'schema',
id: 'test-case-fields',
description: '测试用例字段:名称 / 优先级 / 状态 / 耗时',
fields: [
{ key: 'name', label: '用例名' },
{ key: 'priority', label: '优先级', type: 'P0|P1|P2' },
{ key: 'status', label: '状态', type: 'passed|failed|running|skipped' },
{ key: 'durationMs', label: '耗时(ms)', type: 'number' },
],
})
@@ -0,0 +1,10 @@
/**
* status-badge —— 用例状态徽标 widget(通过/失败/运行中/跳过)
*/
import { registerWidget } from 'vibe-registry'
export const statusBadge = registerWidget({
kind: 'widget',
id: 'status-badge',
description: '用例状态徽标:passed / failed / running / skipped',
})
@@ -0,0 +1,12 @@
/**
* suite-detail-view —— 套件详情 widget
* 渲染套件用例列表 + 状态徽标,支持重跑/停止动作
*/
import { registerWidget } from 'vibe-registry'
export const suiteDetailView = registerWidget({
kind: 'widget',
id: 'suite-detail-view',
schemaId: 'test-case-fields',
description: '套件详情:用例表格 + 状态徽标 + 重跑/停止',
})
@@ -0,0 +1,11 @@
/**
* suite-list —— 测试套件列表 widget
* 展示全部套件与通过率,点击进入套件详情
*/
import { registerWidget } from 'vibe-registry'
export const suiteListWidget = registerWidget({
kind: 'widget',
id: 'suite-list',
description: '测试套件列表:名称 + 最近结果 + 通过率',
})
@@ -0,0 +1,61 @@
/**
* orchestrate —— 编排层验证脚本
* 模拟"渲染内核消费前的编排":集中注册 → 校验 → 执行 setup 数据流 → 输出解析树
*
* 运行:node --experimental-strip-types scripts/orchestrate.ts
*/
import '../registry/index.ts' // ← 集中注册(副作用)
import { registry } from 'vibe-registry'
import { overviewPage } from '../pages/overview.ts'
import { suiteDetailPage } from '../pages/suite-detail.ts'
import { runHistoryPage } from '../pages/run-history.ts'
function dumpRegistry() {
console.log('═══ 注册表(万物皆注册)═══')
for (const e of registry.listAll()) {
const preview = e.kind === 'layout' && e.preview ? ` 预览:「${e.preview.title}」` : ''
console.log(` ${e.kind.padEnd(7)} ${e.id}${preview}`)
}
}
async function orchestrate() {
console.log('═══ 编排层 · 解析树 ═══\n')
const pages = [
{ name: 'overview', page: overviewPage, ctx: { query: {}, params: {} } },
{ name: 'suite-detail', page: suiteDetailPage, ctx: { query: {}, params: { id: 'billing' } } },
{ name: 'run-history', page: runHistoryPage, ctx: { query: {}, params: {} } },
]
for (const { name, page, ctx } of pages) {
const layout = registry.store.layouts.get(page.layout)
if (!layout) throw new Error(`页面 ${name} 引用了未注册布局 ${page.layout}(本不该发生,definePage 已校验)`)
// 业务层:页面只管 setup,数据喂给 blocks
const data = page.setup ? await page.setup(ctx) : {}
console.log(`◆ 页面「${page.title ?? name}」`)
console.log(` layout: ${page.layout} slots=[${layout.slots.join(', ')}]`)
for (const b of page.blocks) {
const widget = registry.store.widgets.get(b.widget)
const bound = b.bind
? Object.entries(b.bind)
.map(([prop, key]) => `${prop}←${key}(${data[key] === undefined ? '?' : Array.isArray(data[key]) ? `${(data[key] as unknown[]).length}项` : typeof data[key]})`)
.join(' ')
: ''
console.log(` └ widget: ${b.widget} props=${JSON.stringify(b.props ?? {})} bind=[${bound}]`)
}
console.log('')
}
}
dumpRegistry()
console.log('')
orchestrate().then(
() => {
console.log('✓ 编排完成:3 页面全部通过校验,setup 数据流正常')
},
(e) => {
console.error('✗ 编排失败:', e)
process.exit(1)
},
)
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"allowImportingTsExtensions": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"types": ["node"]
},
"include": ["registry", "pages", "scripts"]
}
+19
View File
@@ -0,0 +1,19 @@
{
"name": "vibe-registry",
"version": "0.0.1",
"private": true,
"type": "module",
"description": "万物皆注册 · 面向 AI 前端的注册式页面框架(M1 概念验证)",
"main": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"typecheck": "tsc --noEmit",
"vr": "node --experimental-strip-types cli/vr.ts"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.8.0"
}
}
+52
View File
@@ -0,0 +1,52 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
'@types/node':
specifier: ^22.0.0
version: 22.20.1
typescript:
specifier: ^5.8.0
version: 5.9.3
examples/qa-orchestrator:
dependencies:
vibe-registry:
specifier: workspace:*
version: link:../..
devDependencies:
'@types/node':
specifier: ^22.0.0
version: 22.20.1
typescript:
specifier: ^5.8.0
version: 5.9.3
packages:
'@types/node@22.20.1':
resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
snapshots:
'@types/node@22.20.1':
dependencies:
undici-types: 6.21.0
typescript@5.9.3: {}
undici-types@6.21.0: {}
+2
View File
@@ -0,0 +1,2 @@
packages:
- "examples/*"
+23
View File
@@ -0,0 +1,23 @@
/**
* 预设布局:admin(后台管理骨架)
*
* ⚠️ 此文件由 vibe-registry 物化而来:vr add admin 会将其拷贝为
* 项目的 registry/layouts/admin.ts —— 之后它就是你的源码,可随意修改。
* 升级用 vr update,不会静默覆盖你的改动(防覆盖)。
*/
import { registerLayout } from 'vibe-registry'
export default registerLayout({
kind: 'layout',
id: 'admin',
slots: ['sidebar', 'header', 'content', 'footer'],
description: '后台管理骨架:左侧导航 + 顶栏 + 内容区 + 页脚',
preview: {
title: 'Admin · 后台管理',
demoBlocks: [
{ slot: 'sidebar', widget: 'nav-menu' },
{ slot: 'header', widget: 'page-header', props: { title: '订单管理' } },
{ slot: 'content', widget: 'data-table' },
],
},
})
+22
View File
@@ -0,0 +1,22 @@
/**
* 预设布局:app-shell(通用应用外壳)
*
* ⚠️ 此文件由 vibe-registry 物化而来:vr add app-shell 会将其拷贝为
* 项目的 registry/layouts/app-shell.ts —— 之后它就是你的源码,可随意修改。
* 升级用 vr update,不会静默覆盖你的改动(防覆盖)。
*/
import { registerLayout } from 'vibe-registry'
export default registerLayout({
kind: 'layout',
id: 'app-shell',
slots: ['header', 'content'],
description: '通用应用外壳:顶栏 + 内容区(营销/简单页面)',
preview: {
title: 'App Shell · 通用外壳',
demoBlocks: [
{ slot: 'header', widget: 'page-header', props: { title: '欢迎' } },
{ slot: 'content', widget: 'hero-banner' },
],
},
})
+23
View File
@@ -0,0 +1,23 @@
/**
* 预设布局:admin(后台管理骨架)
*
* ⚠️ 此文件由 vibe-registry 物化而来:vr add admin 会将其拷贝为
* 项目的 registry/layouts/admin.ts —— 之后它就是你的源码,可随意修改。
* 升级用 vr update,不会静默覆盖你的改动(防覆盖)。
*/
import { registerLayout } from 'vibe-registry'
export default registerLayout({
kind: 'layout',
id: 'admin',
slots: ['sidebar', 'header', 'content', 'footer'],
description: '后台管理骨架:左侧导航 + 顶栏 + 内容区 + 页脚',
preview: {
title: 'Admin · 后台管理',
demoBlocks: [
{ slot: 'sidebar', widget: 'nav-menu' },
{ slot: 'header', widget: 'page-header', props: { title: '订单管理' } },
{ slot: 'content', widget: 'data-table' },
],
},
})
+72
View File
@@ -0,0 +1,72 @@
/**
* M1 验证脚本:类型 + 注册 + 校验闸门
* 演示:注册布局/widget → definePage 声明 → 引用校验
* 运行:node --experimental-strip-types scripts/smoke.ts
*/
import { createRegistry, registerLayout, registerWidget, definePage } from '../src/index.ts'
const reg = createRegistry()
// 万物皆注册:布局与组件都要先注册,才有 id 可用
reg.registerLayout({
kind: 'layout',
id: 'admin',
slots: ['header', 'content'],
description: '后台骨架',
preview: { title: 'Admin', demoBlocks: [] },
})
reg.registerWidget({
kind: 'widget',
id: 'data-table',
component: 'TableMock', // M1 不接真实 UI,组件占位
description: '数据表格',
})
reg.registerWidget({
kind: 'widget',
id: 'page-header',
component: 'HeaderMock',
description: '页头',
})
// 合法页面:全部引用已注册 id
const page = definePage(
{
layout: 'admin',
title: '订单',
blocks: [
{ widget: 'page-header', props: { title: '订单管理' } },
{ widget: 'data-table', bind: { rows: 'orders' } },
],
async setup() {
return { orders: [{ id: 1, total: 9.9 }] }
},
},
reg,
)
console.log('✓ 合法页面通过校验:', page.title, page.layout, page.blocks.length, 'blocks')
// 非法页面:引用了未注册 widget → 校验闸门必须抛错
try {
definePage(
{
layout: 'admin',
blocks: [{ widget: 'magic-graph' }], // ← 未注册
},
reg,
)
console.error('✗ 应当抛错但没有(校验闸门失效!)')
process.exit(1)
} catch (e) {
console.log('✓ 校验闸门拦截未注册引用:', (e as Error).message.split('\n')[0])
}
// 重复注册同 id → 必须抛错
try {
reg.registerLayout({ kind: 'layout', id: 'admin', slots: [], description: 'dup' })
console.error('✗ 重复注册未抛错(id 唯一性失效!)')
process.exit(1)
} catch (e) {
console.log('✓ 重复注册被拒(id 唯一):', (e as Error).message.split('\n')[0])
}
console.log('smoke OK')
+13
View File
@@ -0,0 +1,13 @@
export { createRegistry, registerLayout, registerWidget, registerAction, registerSchema, registry } from './registry.ts'
export type {
RegistryApi,
RegistryEntry,
RegistryKind,
LayoutRegistryEntry,
WidgetRegistryEntry,
ActionRegistryEntry,
SchemaRegistryEntry,
LayoutPreview,
} from './registry.js'
export { definePage } from './page.ts'
export type { PageDeclaration, PageBlockDecl, PageContext, SetupData } from './page.js'
+56
View File
@@ -0,0 +1,56 @@
/**
* definePage —— 页面声明层
*
* 理念(设计文档 §3.1):
* - 页面只管业务:setup 只做数据/状态,结构全部来自注册表
* - 布局/组件用 id 引用,不写任何布局 JSX
* - 生成产物受控:引用的 id 必须是已注册项(validate 把关)
*/
import { type RegistryApi, registry } from './registry.ts'
/** 页面中一个块的声明:引用已注册 widget,并给 props/绑定 */
export interface PageBlockDecl {
/** widget 注册 id,如 'data-table'、'page-header' */
readonly widget: string
/** 静态 props(可选) */
readonly props?: Record<string, unknown>
/** 业务绑定:setup 返回的数据里取哪些字段喂给 widget(键:widget prop 名,值:setup 结果字段) */
readonly bind?: Record<string, string>
}
/** 页面声明:布局 id + 块列表 + 业务 */
export interface PageDeclaration {
/** 布局注册 id,如 'admin' —— 用户只需说"用 admin 布局" */
readonly layout: string
readonly blocks: readonly PageBlockDecl[]
/** 业务层:返回的数据可供 blocks.bind 使用;只做业务,不碰 UI 结构 */
readonly setup?: (ctx: PageContext) => Promise<Record<string, unknown>> | Record<string, unknown>
/** 行为回调:业务动作(bivariant,允许实现方标注收窄的 payload 类型) */
readonly actions?: Record<string, { bivarianceHack(payload: never, ctx: PageContext): unknown }['bivarianceHack']>
/** 页标题等元信息(可选) */
readonly title?: string
}
export interface PageContext {
readonly query: Record<string, string | undefined>
readonly params: Record<string, string | undefined>
}
/** 把声明物化为"已校验的页面对象";未知引用直接抛错(校验闸门) */
export function definePage(decl: PageDeclaration, reg: RegistryApi = registry): PageDeclaration {
const refs = [
{ kind: 'layout' as const, id: decl.layout },
...decl.blocks.map((b) => ({ kind: 'widget' as const, id: b.widget })),
]
const missing = reg.validateRefs(refs)
if (missing.length > 0) {
throw new Error(
`[vibe-registry] definePage 引用了未注册项:${missing.join(', ')} ` +
`(万物皆注册:请先注册,或用已有注册项)`,
)
}
return decl
}
/** 业务 setup 辅助类型:让页面文件只声明"取哪些数据" */
export type SetupData<P extends PageDeclaration> = Awaited<ReturnType<NonNullable<P['setup']>>>
+146
View File
@@ -0,0 +1,146 @@
/**
* vibe-registry core —— 万物皆注册的类型基石
*
* 设计要点(v0.5):
* - 注册项类型封闭 + id 即引用键
* - 所有注册项可带 demo 预览数据(预览即真实)
* - 纯 TS、零依赖、不接 UI(渲染内核后续接)
*/
export type RegistryKind = 'layout' | 'widget' | 'action' | 'schema' | 'theme'
/** 布局骨架:页面级结构。渲染内核据 slots 填充 blocks。 */
export interface LayoutRegistryEntry {
readonly kind: 'layout'
/** 简易 id,如 'admin' —— 用户/agent 只需说这个 id */
readonly id: string
/** 布局所含槽位顺序,渲染时 blocks 依 slot 填入 */
readonly slots: readonly string[]
/** 描述,供画廊/agent 展示 */
readonly description: string
/** demo 数据(预览即真实,与实装共用渲染内核) */
readonly preview?: LayoutPreview
}
export interface LayoutPreview {
/** 标题文本,画廊首页展示 */
readonly title: string
/** 缩略示意:每个 slot 放什么 demo block(id 或描述) */
readonly demoBlocks?: ReadonlyArray<{
readonly slot: string
readonly widget: string
readonly props?: Record<string, unknown>
}>
}
/** 业务组件注册项(映射到已有 UI 库,不重造原子组件) */
export interface WidgetRegistryEntry {
readonly kind: 'widget'
readonly id: string
/** 渲染组件。内核薄,仅做映射层 */
readonly component?: unknown
/** 关联字段/列 schema id(可选) */
readonly schemaId?: string
readonly description?: string
}
/** 行为注册项:可复用的事件语义(泛型:payload/结果类型随注册保留,不丢业务语义) */
export interface ActionRegistryEntry<P = unknown, R = unknown> {
readonly kind: 'action'
readonly id: string
readonly handler: (payload: P, ctx: unknown) => R | Promise<R>
readonly description?: string
}
/** 数据/字段注册项:表单字段、表格列 */
export interface SchemaRegistryEntry {
readonly kind: 'schema'
readonly id: string
readonly fields?: readonly unknown[]
readonly description?: string
}
export type RegistryEntry =
| LayoutRegistryEntry
| WidgetRegistryEntry
| ActionRegistryEntry
| SchemaRegistryEntry
/** 注册表集合:kind → id → entry(单一事实来源) */
export type RegistryStore = {
readonly layouts: ReadonlyMap<string, LayoutRegistryEntry>
readonly widgets: ReadonlyMap<string, WidgetRegistryEntry>
readonly actions: ReadonlyMap<string, ActionRegistryEntry>
readonly schemas: ReadonlyMap<string, SchemaRegistryEntry>
}
/* ── 注册与查询 ─────────────────────────────────────────────── */
export type RegisterFn<K extends RegistryKind, E extends { readonly kind: K; readonly id: string }> =
(entry: E) => E
export interface RegistryApi {
readonly store: RegistryStore
registerLayout: RegisterFn<'layout', LayoutRegistryEntry>
registerWidget: RegisterFn<'widget', WidgetRegistryEntry>
registerAction: <P, R>(entry: ActionRegistryEntry<P, R>) => ActionRegistryEntry<P, R>
registerSchema: RegisterFn<'schema', SchemaRegistryEntry>
/** 校验:给定引用集合,返回未注册的 id 清单(空 = 通过) */
validateRefs: (refs: ReadonlyArray<{ readonly kind: RegistryKind; readonly id: string }>) => string[]
/** 画廊用的清单:全部已注册项(dev 用) */
listAll: () => RegistryEntry[]
}
/** 创建注册表(单例由 createRegistry 返回) */
export function createRegistry(): RegistryApi {
const layouts = new Map<string, LayoutRegistryEntry>()
const widgets = new Map<string, WidgetRegistryEntry>()
const actions = new Map<string, ActionRegistryEntry>()
const schemas = new Map<string, SchemaRegistryEntry>()
function register<K extends RegistryKind, E extends { readonly kind: K; readonly id: string }>(
map: Map<string, RegistryEntry>,
entry: E,
): E {
const existed = map.get(entry.id)
if (existed) {
throw new Error(`[vibe-registry] 注册冲突:${entry.kind} '${entry.id}' 已存在(万物皆注册,id 唯一)`)
}
map.set(entry.id, entry as RegistryEntry)
return entry
}
return {
store: { layouts, widgets, actions, schemas },
registerLayout: (e) => register(layouts, e),
registerWidget: (e) => register(widgets, e),
registerAction: <P, R>(entry: ActionRegistryEntry<P, R>) => {
if (actions.has(entry.id)) {
throw new Error(`[vibe-registry] 注册冲突:action '${entry.id}' 已存在(万物皆注册,id 唯一)`)
}
actions.set(entry.id, entry as ActionRegistryEntry)
return entry
},
registerSchema: (e) => register(schemas, e),
validateRefs(refs) {
const missing: string[] = []
for (const r of refs) {
const map = r.kind === 'layout' ? layouts : r.kind === 'widget' ? widgets : r.kind === 'action' ? actions : schemas
if (!map.has(r.id)) missing.push(`${r.kind}:${r.id}`)
}
return missing
},
listAll() {
return [...layouts.values(), ...widgets.values(), ...actions.values(), ...schemas.values()]
},
}
}
/* ── 全局注册表(单例,项目内 import 即用) ────────────────────── */
const globalRegistry = createRegistry()
export const registerLayout: RegistryApi['registerLayout'] = (e) => globalRegistry.registerLayout(e)
export const registerWidget: RegistryApi['registerWidget'] = (e) => globalRegistry.registerWidget(e)
export const registerAction: RegistryApi['registerAction'] = (e) => globalRegistry.registerAction(e)
export const registerSchema: RegistryApi['registerSchema'] = (e) => globalRegistry.registerSchema(e)
export const registry: RegistryApi = globalRegistry
+19
View File
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"allowImportingTsExtensions": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"types": ["node"],
"baseUrl": ".",
"paths": {
"vibe-registry": ["./src/index.ts"]
}
},
"include": ["src", "cli", "presets", "registry"]
}