ITADN
voidzero-dev/pkg-pr-registry-bridge
README.md
以下内容由 AI 翻译,如有问题请点此提交 issue 反馈

pkg-pr-registry-bridge

一个基于版本控制的 npm registry 桥接服务,允许包管理器使用标准的 npm registry 语义安装 pkg.pr.new Vite+ 预览构建。作为单个 Cloudflare Worker 运行。

线上地址:https://registry-bridge.viteplus.dev

包名用于选择上游包;版本模式用于选择 来源:

@voidzero-dev/vite-plus-core@0.0.0-commit.a832a55  -> pkg.pr.new commit build
vite-plus@0.0.0-commit.a832a55                     -> pkg.pr.new commit build
vite-plus@0.2.1, react@latest                      -> npm registry

仅支持不可变的提交构建0.0.0-commit.<sha>)。PR 编号 版本(0.0.0-pr.<n>)会被有意拒绝:PR 引用是可变的(它会 推进到更新的提交),因此其生成的元数据/压缩包会被 覆盖,并可能与消费者已在锁文件中固定的内容不匹配。 固定提交 sha 可保持内容不可变。

这使得 Bun 别名覆盖能够通过桥接正常工作:

{
  "overrides": {
    "vite": "npm:@voidzero-dev/vite-plus-core@0.0.0-commit.a832a55"
  }
}

参见 rfcs/0001-pkg-pr-new-registry-bridge-cloudflare-workers.md 以获取完整设计,以及 examples/bun-validation 以获取可运行的示例。

要为其他项目运行预览构建,请 fork 并重新配置:上游 仓库、包允许列表和 origin 均为配置项。参见 docs/self-hosting.md

为什么需要独立的 registry

npm 将包的 packument(列出所有版本的元数据文档)限制在 未压缩状态下 100 MB,并且客户端在每次全新安装时都会完整下载它。 每次提交都会消耗该预算:Drizzle ORM 在大约 763 次 发布后触及上限,随后 registry 阻止了新的发布约一个月(vlt.io on packument size limits)。 该桥接服务将每次提交的预览从 npm 的 packument 中隔离出来。它从一个 独立的 registry 提供这些预览,作为基于有界且经过 TTL 修剪的 ref 集合的合成版本,从而 保持真实的 packument 小巧,并允许预览以正常的 npm 语义进行安装。

工作原理

  • Packument (GET /vite-plus, GET /@voidzero-dev/vite-plus-core): 获取 npm packument(如果该包不在 npm 上,则合成一个空的 packument), 注入配置的预览版本,并保持现有版本和 latest 不变。
  • Tarball (GET /tarballs/<pkg>/<version>.tgz): 直接从 R2 提供, 作为唯一的事实来源。制品在 CI 中构建并计算哈希(即 下方的发布操作)并上传,因此 Worker 仅流式传输字节,它从不 解压缩或计算负载的哈希,因此无论大小如何,它都不会触及 Worker 的 CPU/内存限制。 相同的对象在 npm 约定路径 (GET /<pkg>/-/<name>-<version>.tgz) 处提供,供那些合成 该 URL 而不是读取 dist.tarball 的客户端和锁文件使用;那里的非预览包/版本 会被重定向到 npm。
  • Transitive deps: 预览构建的 optionalDependencies(平台 二进制文件)被固定到相同的合成 version string (0.0.0-commit.<sha>),并且桥接服务也为这些包提供 packument, 因此它们像其他预览包一样通过桥接服务解析,并且 包管理器仅下载当前平台的二进制文件 (从 packument 中读取 os/cpu),而不是下载所有二进制文件。发布 操作将同一发布批次中任何依赖于该包的情况固定。二进制文件 很大(几十 MB),因此它们在 CI 中打包 + 计算哈希(那里没有 每请求限制)并上传;二进制文件的 package.json 版本被 重写为合成版本,以匹配解析器所期望的版本。 (pnpm 的严格存储检查会拒绝不匹配)。所有内容均从 R2 提供: 字节不存在的版本将返回 404,Worker 绝不会按需构建 tarball 或 重定向到 pkg.pr.new。
  • 发布POST /-/publish, PUT /-/tarball/...): publish action 将每个本地构建的包 目录(在生成产物的同一 CI 作业中,无需 pkg.pr.new 往返)进行打包,重写 + 重新打包 + 计算哈希,PUT 字节,并 POST 元数据(重写后的 package.json + 完整性校验值),并注册 ref,全部 在一次 CI 运行中完成。 由于完整性校验值是针对提供的确切字节计算的,因此每个验证它的包 管理器(npm, pnpm, yarn)都能匹配,并且 bun/yarn-berry 会在首次安装时 固定该值。
  • 其他所有内容:302 重定向到 registry.npmjs.org,因此客户端 直接从 npm 的 CDN 获取典型安装中的数百个普通包。Worker 不会 介入其未合成的任何内容的数据路径。

@voidzero-dev/vite-plus-corevite-plus 接收合成预览版本(严格允许列表)。Owner/repo 固定为 voidzero-dev/vite-plus

消费者配置(重要)

bunfig.toml:

[install]
registry = "https://registry-bridge.viteplus.dev/"

# REQUIRED for large installs. Bun's default network concurrency (48) triggers
# an HTTP/2 client bug against Cloudflare on big dependency graphs (vite-plus
# pulls 400+ packages): streams get dropped and resolution fails with "no
# version matching". Capping concurrency avoids it. The bridge serves correct
# responses; this is a bun-side workaround.
networkConcurrency = 8

package.json(优先使用不可变提交构建以实现可复现性):

{
  "devDependencies": {
    "vite": "npm:@voidzero-dev/vite-plus-core@0.0.0-commit.<sha>",
    "@voidzero-dev/vite-plus-core": "0.0.0-commit.<sha>",
    "vite-plus": "0.0.0-commit.<sha>"
  },
  "overrides": {
    "vite": "npm:@voidzero-dev/vite-plus-core@0.0.0-commit.<sha>"
  }
}

关于注册表环境变量覆盖的说明:bun 会优先使用 npm_config_registry(pnpm/npm 例如从 PNPM_CONFIG_REGISTRY 派生而来),而非 bunfig.toml。如果 你通过其他包管理器的脚本运行 bun install 并且配置了注册表镜像,请取消该覆盖设置或直接运行 bun,以便使用桥接 注册表。

为什么桥接需要一个预览引用列表

包管理器会获取 packumentGET /vite-plus)以在解析版本之前发现哪些 版本存在,且该请求不包含期望版本的提示。因此,桥接必须知道要在该 packument 中列出哪些合成预览版本。 pkg.pr.new 没有 API 可以将其构建枚举为 semver 版本,因此该集合是显式维护的。

相比之下,tarball 端点无需配置即可接受任何有效的预览版本;只有基于 packument 的发现需要该列表。

引用通过以下管理端点在运行时注册(发布操作从 CI 中调用它们),存储在一个通过廉价的 get 读取的单个 R2 索引对象中(而非受速率限制的 KV list),并通过 TTL 进行修剪。无需重新部署,也无需静态 配置:一旦操作注册了已发布的预览,它就会立即出现。

对于从 PR 发布的引用(该操作会转发 PR url),提供的 packument 还会暴露一个指向该 PR 最新已发布提交版本的 pr-<n> dist-tag。每个提交的版本保持不可变;随着 PR 的推进,该标签 会移动,因此针对此 注册表执行 npm/pnpm install <pkg>@pr-<n> 将安装该 PR 的头部构建。

直接 tarball 下载(pkg.pr.new 风格)

A pkg.pr.new 风格的 URL 将 ref 解析为 tarball,便于 curl 下载 或单个自包含的包。替换任何 pkg.pr.new URL 上的主机名:

# The repo's main package (vite-plus) at a PR's latest commit, or a commit sha:
curl -L https://registry-bridge.viteplus.dev/voidzero-dev/vite-plus@1891
curl -L https://registry-bridge.viteplus.dev/voidzero-dev/vite-plus@<sha>
# A specific (here scoped) package:
curl -L https://registry-bridge.viteplus.dev/voidzero-dev/vite-plus/@voidzero-dev/vite-plus-core@1891

A GET 302-redirects to the canonical /tarballs/<pkg>/<version>.tgz. A HEAD answers 200 (no body) and resolves the ref to its exact commit via pkg.pr.new-style headers, so a tool can pin a (mutable) PR number to a commit without downloading:

curl -I https://registry-bridge.viteplus.dev/voidzero-dev/vite-plus@1891
# HTTP/2 200
# x-commit-key: voidzero-dev:vite-plus:<sha>
# x-pkg-name-key: vite-plus

GETHEAD 均包含 x-commit-key/x-pkg-name-key。请注意,已发布的预览版 tarball 的传递依赖项被固定为版本号(而非 pkg.pr.new URL), 因此,若要完整安装包含其平台二进制的元包,请使用上述的 registry + pr-<n> 标签,而不是将此 URL 作为裸依赖项。

Admin endpoints

写入操作受 Authorization: Bearer <ADMIN_TOKEN> 保护(使用 void secret put ADMIN_TOKEN 设置 ADMIN_TOKEN);若未配置,写入端点 将返回 503。GET /-/refs 是公开读取。

# List registered refs - no auth required.
# Each entry: { ref, version, publishedAt, prUrl, expiresAt }. publishedAt is the
# server-stamped release date; prUrl is null unless published from a PR; expiresAt
# is the index TTL (90 days out).
curl https://.../-/refs

# Purge a generated build (its tarball + meta) from R2
curl -X POST -H "authorization: Bearer $ADMIN_TOKEN" -H 'content-type: application/json' \
  -d '{"package":"vite-plus","version":"0.0.0-commit.a832a55"}' https://.../-/purge

Refs 通过发布创建:tarball 上传(PUT /-/tarball/<pkg>/<version>.tgz) 然后 POST /-/publish(存储元数据 + 注册 ref),两者均受管理员保护 并由 publish action 驱动,而非手动操作。

已发布的 ref 会立即生效,并在下一次请求时构建到 packument 中。这是暴露新预览构建的无需重新部署路径。

从 CI 发布

繁重的工作(打包、重写、重新打包、哈希)通过可复用 action 在 CI 中运行,在与构建工件相同的作业中,因此 Worker 仅提供服务 且 pkg.pr.new 不参与。将其集成到 vite-plus 的发布工作流中:参见 docs/ci-setup.md。要手动发布(相同的代码路径), 运行 PKG_PR_BRIDGE_ADMIN_TOKEN=… pnpm warm --repo <built-vite-plus-checkout> <sha>

该 action 的 bundle 已提交 (.github/actions/publish-preview/dist/index.mjs);在更改 action 或其导入的任何模块后,使用 pnpm build:action 重新构建它。

配置

非机密值在 env.ts 中声明(类型化并验证)并在 .env 中设置(已提交),在 .env.production 中按环境覆盖。机密 使用 void secret put 上传:

变量含义
PUBLIC_BASE_URL桥接的公共源;用于 dist.tarball URL。必须与已部署的路由匹配。
NPM_REGISTRYnpm 回退注册表(https://registry.npmjs.org)。
PREVIEW_OWNER / PREVIEW_REPO固定的上游仓库(voidzero-dev / vite-plus)。
WORKSPACE_PACKAGEStarball 端点的允许列表。精确名称或 prefix*,例如 vite-plus,@voidzero-dev/vite-plus-*

绑定/机密:

  • STORAGE (R2) - 生成的 tarball、重写后的元数据(包括完整性校验)以及运行时注册的 refs 索引。由 Void 在部署时自动配置(无需手动创建 bucket);该绑定在 void.jsoninference.bindings.storage)中声明。运行时 refs 索引在 90 天后自动过期(代码内 TTL),并且每日运行的 Void cron(crons/cleanup-expired.ts)会在其 ref TTL 过期后清理各版本的 meta/tarball 对象,从而将 R2 存储限制在活跃 ref 窗口内。
  • ADMIN_TOKEN(secret)- 保护管理端点。使用 void secret put ADMIN_TOKEN 设置。

Develop

这是一个 Void 应用:voidPlugin() 位于 vite.config.tsroutes/ 层构建 Worker,该 Worker 将每个请求转发到 位于 src/app.ts 的 Hono 注册应用。Void 推断出 STORAGE R2 绑定, 并将 .env* 加载到 Worker 的 vars 中。

pnpm install       # also runs `void prepare` (generates .void/ types)
pnpm typecheck
pnpm test          # vitest, runs the worker in workerd (Miniflare)
pnpm dev           # `vite dev` (local worker via Miniflare, http://localhost:5173)

用于本地管理员测试,请将 ADMIN_TOKEN=… 放入 .env.local(已被 git 忽略)。

部署

部署到 Void 托管平台,使用 void deploy; Void 会配置 Worker 和 STORAGE R2 存储桶(无需 Cloudflare 账户)。 若要为另一个项目运行独立的桥接(fork、配置、 部署、接入 CI),请遵循 docs/self-hosting.md

# One-time: authenticate and set the admin secret on the project.
void auth login
void secret put ADMIN_TOKEN              # guards the admin write endpoints

# Deploy and run the end-to-end bun install check.
# Use `pnpm run deploy` (not `pnpm deploy`, which is pnpm's built-in command).
pnpm run deploy                          # void deploy + e2e

pnpm run deploy 运行 void deploy,然后运行 pnpm test:e2e(针对实时 bridge 的真实 bun install,断言 alias/override 解析 到合成版本,使用 bridge 已提供的 ref)。仅对 void deploy 使用 pnpm run deploy:only

公共源(.env.production 中的 PUBLIC_BASE_URL)是自定义域名 https://registry-bridge.viteplus.dev,通过 void domain add 附加( 底层 Void 平台 URL pkg-pr-registry-bridge.void.app 也保持 可用)。

CI 分两个阶段部署,两者都对真实的 Void 运行时进行冒烟测试,而 pool-workers 单元测试无法模拟(例如,平台禁止 caches.default,这导致所有 packument 返回 500,而所有单元测试均通过):

  • 每个 PR.github/workflows/staging.yml)将更改部署到 共享的 staging 项目(pkg-pr-registry-bridge-staging.void.app)并 对其运行 scripts/smoke-test.mjs。将 Staging 设为必需的 status check(分支保护),以便失败的冒烟测试阻止合并。(对于 fork PR 跳过,因为它们无法读取 VOID_TOKEN。)
  • 推送到 main.github/workflows/void-deploy.yml)重新运行 staging 部署 + 冒烟测试作为门禁,然后部署到生产环境并对其进行冒烟测试。该 门禁是必要的,因为更改可能在没有 PR 检查的情况下到达 main,例如 fork PR 或直接推送,因此除非 staging 首先通过,否则生产环境永远不会发布。

冒烟测试访问 /_health/vite-plus packument(200 且带有 time)、 /-/refs 以及下载重定向。

推送到main的工作流使用 GitHub OIDC 进行身份验证:void deploy 将短期 OIDC 令牌交换为项目范围的部署令牌,因此不涉及任何 密钥。这要求每个项目将仓库连接一次 (void github connect <project> --repo voidzero-dev/pkg-pr-registry-bridge --branch main --executor github_actions),并且工作流文件的名称必须 完全为void-deploy.yml。PR 预发布部署仍然需要一个VOID_TOKEN 仓库密钥(void auth token 会将其复制到你的剪贴板): 平台拒绝为 pull_request 事件生成部署令牌,因为这些事件运行的是 不受信任的代码。使用pnpm smoke <url>在本地运行冒烟测试,并使用pnpm deploy:staging手动部署 预发布环境。

License

MIT