跳转到内容
关系图谱

repo-platform-kit:仓库平台统一取数内核

2,035 字 7 分钟

属性 1
series
VaultKit 包指南

@vaultkit/repo-platform-kit 是代码托管平台(GitHub / CNB)的统一取数内核:URL 解析、raw 内容与仓库元数据的请求构造、认证策略、带韧性的 TTL 磁盘缓存、平台徽标常量,全部收敛在一个零依赖的 Node 库里。它本身不依赖 VitePress,是 repo-cardremote-filenpm-card 三个插件的共享底座,任何 Node 构建期工具都可直接消费。

本文是 API 文档型文章:全部导出函数、类型、常量逐一详解,附鉴权对照表、缓存机制剖析,以及如何基于 Provider 五方法扩展新平台。

为什么需要它

repo-card 要抓仓库元数据,remote-file 要抓文件 raw 内容——两者都要解析 GitHub / CNB 的各种 URL 形态、都要处理认证、都要缓存。这些「平台知识」若在每个插件里各写一份,GitHub 改个 URL 形态就要修三处,CNB 的认证坑(下文详述)也要踩三遍。

repo-platform-kit 把平台知识收敛到单点:每个平台一份 Provider 实现,五个方法覆盖「解析 → 构造请求 → 回链」全链路;缓存与认证策略与平台实现正交复用。效果可以直接看——下面这张卡片和这段代码,就是站在这个底座上渲染出来的:

package.json
json
{
  "name": "vitepress",
  "version": "2.0.0-alpha.18",
  "description": "Vite & Vue powered static site generator",
  "keywords": [

安装

sh
pnpm add -D @vaultkit/repo-platform-kit

零运行时依赖——只用 Node 内置模块(node:fs / node:crypto / node:path)与全局 fetch。注意它是 Node 侧库,仅供构建期消费,不可打进客户端产物。

快速上手

三十秒抓一个 GitHub 文件(带磁盘缓存):

ts
// build.ts
import { defaultCacheDir, fetchCached, parseFileUrl, PROVIDERS } from '@vaultkit/repo-platform-kit'

const ref = parseFileUrl('https://github.com/vuejs/core/blob/main/README.md#L1-L20')
if (ref) {
  const request = PROVIDERS[ref.platform].fileRawRequest(ref, {})
  const content = await fetchCached(request, {
    ttl: 3_600_000,
    cacheDir: defaultCacheDir('my-tool'),
    tag: 'my-tool',
  })
  console.log(content)
}

类型全览

Platform

ts
type Platform = 'github' | 'cnb'

平台字面量联合,全库的判别键。

FileRef

平台文件坐标——parseFileUrl 的产物、文件类请求构造的输入:

ts
interface FileRef {
  platform: Platform
  repo: string // 仓库路径;CNB 可含多级组织 org/sub/repo
  ref: string // 分支名或 commit SHA
  path: string // 仓库内文件路径
  start?: number // 行区间起点(来自 URL 的 #L10-L40 片段)
  end?: number // 行区间终点;只给 #L10 时 start 与 end 同值
}

RepoRef

仓库坐标,parseRepoUrl 的产物:{ platform, repo }

AuthOptions

认证选项,三个字段全部可缺省(回退环境变量 / 官方基址):

ts
interface AuthOptions {
  githubToken?: string // 缺省读 GITHUB_TOKEN,可选,仅提额
  cnbToken?: string // 缺省读 CNB_TOKEN,CNB 场景必需
  cnbApiBase?: string // 缺省 https://api.cnb.cool
}

HttpRequest

请求描述 { url, headers }——Provider 只构造请求不发请求,发与缓存交给 fetchCached。构造与执行分离,消费方也可以拿 HttpRequest 自己发。

CacheContext

缓存上下文 { ttl, cacheDir, tag }ttl 单位 ms;cacheDir 缓存目录;tag 用于日志与报错归属(如 'repo-card')。

常量全览

PROVIDERS

Record<Platform, Provider>——平台知识的本体,每份 Provider 实现五个方法(见下节)。

ATTR_PATTERN

{key="value" key2=bare} 花括号属性的解析正则,三个消费插件共用同一口径。parseAttrs 是它的便捷包装。

PLATFORM_MARKS

Record<Platform, string>——GitHub / CNB 的 simple-icons 形 SVG 徽标字符串。填色用 currentColor,品牌色(CNB 橙 #FF6200 等)由消费方 CSS 上色,同一份 SVG 可用于任意配色场景。

函数全览

parseFileUrl(url)

(url: string) => FileRef | null——全平台逐一尝试解析文件 URL,支持的形态:

  • github.com/<o>/<r>/blob/<ref>/<path>raw 路径、refs/heads/ 前缀、permalink SHA 均可)
  • raw.githubusercontent.com/<o>/<r>/<ref>/<path>
  • cnb.cool/<repo>/-/blob/<ref>/<path>/-/raw//-/git/raw/ 亦可)

#L10-L40 片段解析进 start / end。不匹配返回 null(不抛错,交消费方定错误文案)。

parseRepoUrl(url)

(url: string) => RepoRef | null——解析仓库主页 URL。刻意排除文件页:GitHub 的 /blob//raw/ 形态与 CNB 的 /-/ 形态返回 null,避免「把文件链接当仓库」的静默误判。

fetchCached(request, ctx)

(request: HttpRequest, ctx: CacheContext) => Promise<string>——带韧性的抓取,行为分四层:

  1. 缓存文件(cacheDir/sha1(url).json,存 { content, fetchedAt })在 TTL 内 → 直接返回,不发请求
  2. 过期 → 重抓,成功则回写缓存
  3. 重抓失败且有陈缓存 → 回退陈缓存console.warn,构建照常
  4. 失败且无缓存 → 抛错 fail-fast,报错自带行动指引

第 3 层是弱网 / 限额韧性的关键:已经构建成功过的站点,上游抖动不会让它突然构建失败。

defaultCacheDir(name)

(name: string) => string——约定缓存目录 <cwd>/node_modules/.cache/<name>。CI 上建议把这个目录持久化,配合陈缓存回退可把外部 API 请求压到近乎为零。

requireCnbToken(auth, tag)

(auth: AuthOptions, tag: string) => string——解析 CNB 令牌(auth.cnbToken ?? env.CNB_TOKEN),缺失时抛出带配置指引的错误,tag 标注报错归属插件。单独导出是因为 CNB 令牌是「必需项」,值得一个语义明确的入口。

parseAttrs(text)

(text: string) => Record<string, string>——解析花括号属性文本为键值表,接受引号值与裸值。

Provider 五方法

Provider 是平台知识的接口契约(内部接口,经 PROVIDERS 暴露实例):

方法签名职责
parseFileUrl(url) => FileRef | null识别本平台的文件 URL 形态
parseRepoUrl(url) => RepoRef | null识别仓库主页 URL(排除文件页)
fileRawRequest(ref, auth) => HttpRequest构造文件 raw 内容请求(URL + 认证头)
fileHtmlUrl(ref) => stringFileRef → 平台网页版地址(出处回链用)
repoMetaRequest(ref, auth) => HttpRequest构造仓库元数据 API 请求

两平台的落点对照:

职责GitHubCNB
raw 内容raw.githubusercontent.com/...api.cnb.cool/<repo>/-/git/raw/<ref>/<path>
仓库元数据api.github.com/repos/<repo>api.cnb.cool/<repo>
认证头有 token 才带 Authorization恒带 Authorization(无 token 即抛错)

鉴权对照表

平台令牌来源必要性
GitHubAuthOptions.githubToken ?? 环境变量 GITHUB_TOKEN可选。匿名限额 60 次/时,带令牌提至 5000 次/时
CNBAuthOptions.cnbToken ?? 环境变量 CNB_TOKEN必需。CNB API 连公开仓库也要求认证(实测 401),令牌权限需含 repo-code:r

配套的错误提示也内置在 fetchCached 里:GitHub 403 时提示「设置 GITHUB_TOKEN 提额」,CNB 401 时提示「令牌需含 repo-code:r 权限」——报错即文档。

作为三个插件的底座

三个消费插件各取所需,恰好画出这个库的分层:

消费方平台知识属性解析缓存徽标
repo-cardparseRepoUrl + requireCnbTokenATTR_PATTERNfetchCached + defaultCacheDirPLATFORM_MARKS
remote-fileparseFileUrl + PROVIDERS(raw 请求 / 回链)parseAttrs同上PLATFORM_MARKS
npm-card无(npm 不是代码托管平台)ATTR_PATTERN同上自带 npm 徽标

npm-card 一行平台代码都不用,仍复用缓存与属性解析——说明「平台层」与「基础设施层」在这个库里是正交的:前者是 PROVIDERS,后者是 fetchCached / parseAttrs / defaultCacheDir

如何扩展新平台

想支持 Gitee / GitLab?新增平台 = 补一份 Provider 实现,五个方法照抄结构:

ts
const gitee: Provider = {
  parseFileUrl(url) {
    // 识别 gitee.com/<o>/<r>/blob/<ref>/<path>(含 #L 片段)
    const m = url.match(GITEE_BLOB)
    return m ? fileRefOf('gitee', `${m[1]}/${m[2]}`, m[3], m[4], m[5], m[6]) : null
  },
  parseRepoUrl(url) {
    // 仓库主页形态,记得排除 /blob/ 文件页
  },
  fileRawRequest(ref, auth) {
    return { url: `https://gitee.com/${ref.repo}/raw/${ref.ref}/${ref.path}`, headers: {} }
  },
  fileHtmlUrl(ref) {
    return `https://gitee.com/${ref.repo}/blob/${ref.ref}/${ref.path}`
  },
  repoMetaRequest(ref, auth) {
    return { url: `https://gitee.com/api/v5/repos/${ref.repo}`, headers: {} }
  },
}

再把 'gitee' 加进 Platform 联合、把实例挂进 PROVIDERS——顶层的 parseFileUrl / parseRepoUrl 是对全平台的逐一尝试循环,新平台自动被纳入;上层插件零改动获得新平台支持(当前 Platform 是封闭联合,扩展经 PR / fork 进行,这是刻意的:平台知识必须经过测试收编,不开运行时注册口子)。

设计要点:Provider 方法只做纯函数式的解析与构造,不发请求、不读缓存——副作用全部集中在 fetchCached。这让新平台的实现无需关心缓存与韧性,也让五个方法都可以独立单测。

常见问题

为什么不用 octokit? 需求只有「一个 GET + 认证头」,octokit 及其依赖树对构建期工具是过度工程;零依赖意味着安装快、供应链面小。

缓存会无限膨胀吗? 缓存按 URL 数量线性增长,站点引用的 URL 集合有限;node_modules/.cache 随依赖重装自然清理,也可随时手动删。

能在浏览器里用吗? 不能,它用了 node:fs / node:crypto。浏览器场景请在服务端取好数再下发。

相关链接