From 69900822477a8e9b17d86d1e97416998e0713c7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD?= <3401797899@qq.com> Date: Sun, 26 Jul 2026 20:23:15 +0800 Subject: [PATCH] =?UTF-8?q?build(deploy):=20=E9=83=A8=E7=BD=B2=E4=B8=8E?= =?UTF-8?q?=E9=99=90=E6=B5=81=E9=85=8D=E7=BD=AE=E5=88=87=E6=8D=A2=E5=88=B0?= =?UTF-8?q?=20filelib-web,=E5=B9=B6=E5=8A=A0=E5=85=B1=E5=AD=98=E5=9B=9E?= =?UTF-8?q?=E5=BD=92=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三处引用旧工程名/旧资源路径的地方一并更新,它们必须同时改 —— 少改一处 就是静默故障,而不是构建期报错: 1. 部署脚本(deploy_platform.sh / deploy_fleet_release.sh):npm ci 的 prefix、rsync 排除项、构建产物存在性检查从 database-admin 换成 filelib-web。最后一项是真门禁:static.ts 缺产物时只 warn 不注册路由, 漏改会让 /app 与 /database 静默 404 —— 恰是 database-admin 长期处于 禁用状态的原因。 2. silo 限流豁免:资源路径随 appDir 改名而变(/database/_app/* 已不存在, 现为 /_filelib/*);/app/* 此前不在豁免列表,它现在也是 SPA 外壳, 客户端路由无法预先枚举。 注:/database/* 是整体豁免,filelib 的 JSON API 也绕过限流预算。这是 迁移前就有的行为,原样保留,但覆盖面因多了 /app/* 而变宽。 3. 回归测试:把 registerStaticSpa 与 registerDatabaseSpa 挂到同一个 Fastify 实例,断言 ready() 不因重复路由抛错 —— appDir 若用回默认的 _app,这里会红(ADR-0029 的承重约束)。另断言 /app 与 /database/dashboard/users 返回同一份字节(SPA 回退不读请求)、body 含 /_filelib/。构建产物缺失时不 skip 而是直接失败:那说明该先跑 filelib-web 的 build,不是测试不适用。 --- hub/deploy/deploy_fleet_release.sh | 8 ++-- hub/deploy/deploy_platform.sh | 6 +-- hub/src/deployment/siloRateLimit.ts | 14 +++---- hub/test/unit/silo-rate-limit.test.ts | 11 +++++ hub/test/unit/spa-route-registration.test.ts | 43 ++++++++++++++++++++ 5 files changed, 68 insertions(+), 14 deletions(-) create mode 100644 hub/test/unit/spa-route-registration.test.ts diff --git a/hub/deploy/deploy_fleet_release.sh b/hub/deploy/deploy_fleet_release.sh index 53d2148..2d52e9b 100755 --- a/hub/deploy/deploy_fleet_release.sh +++ b/hub/deploy/deploy_fleet_release.sh @@ -82,11 +82,11 @@ REMOTE rsync -az --delete \ --exclude node_modules --exclude dist --exclude .env \ --exclude admin-web/node_modules --exclude admin-web/build --exclude admin-web/.svelte-kit \ - --exclude database-admin/node_modules --exclude database-admin/build --exclude database-admin/.svelte-kit \ + --exclude filelib-web/node_modules --exclude filelib-web/build --exclude filelib-web/.svelte-kit \ -e "ssh ${SSH_OPTS[*]}" \ "$REPO_ROOT/hub/" "$DEPLOY_USER@$HOST:$HUB_DIR/" - echo "[fleet] npm ci + build (tsc + admin-web & database-admin SPAs)" + echo "[fleet] npm ci + build (tsc + admin-web & filelib-web SPAs)" ssh "${SSH_OPTS[@]}" "$DEPLOY_USER@$HOST" bash -s < { expect(isSiloHttpRateLimitExempt("/admin/org/para-26071100/members")).toBe(true); }); + it("exempts the filelib-web SPA assets and both of its mount prefixes", () => { + // appDir 改名为 `_filelib`(见 src/database/static.ts):同一份产物服务 + // 老师端 /app 与管理后台 /database。 + expect(isSiloHttpRateLimitExempt("/_filelib/version.json")).toBe(true); + expect(isSiloHttpRateLimitExempt("/_filelib/immutable/chunks/foo.js?v=1")).toBe(true); + expect(isSiloHttpRateLimitExempt("/app")).toBe(true); + expect(isSiloHttpRateLimitExempt("/app/dev-login-teacher")).toBe(true); + expect(isSiloHttpRateLimitExempt("/database")).toBe(true); + expect(isSiloHttpRateLimitExempt("/database/dashboard/users")).toBe(true); + }); + it("still rate-limits APIs and auth", () => { expect(isSiloHttpRateLimitExempt("/api/me")).toBe(false); expect(isSiloHttpRateLimitExempt("/api/org/x/members")).toBe(false); diff --git a/hub/test/unit/spa-route-registration.test.ts b/hub/test/unit/spa-route-registration.test.ts new file mode 100644 index 0000000..38f6cdb --- /dev/null +++ b/hub/test/unit/spa-route-registration.test.ts @@ -0,0 +1,43 @@ +/** + * 两套 SPA 静态托管共存于同一个 Fastify 实例。 + * + * 这是 ADR-0029 的承重约束的回归测试:filelib-web 的 appDir 若用 SvelteKit 默认的 + * `_app`,就会与 admin-web 在根上注册的 `/_app/*` 撞成重复路由,Fastify 启动即抛错。 + * 改名为 `_filelib` 后两者共存 —— 这里把两个注册函数挂到同一实例上验证。 + */ +import Fastify from "fastify"; +import { describe, expect, it } from "vitest"; +import { registerStaticSpa } from "../../src/admin/static.js"; +import { registerDatabaseSpa } from "../../src/database/static.js"; + +describe("SPA static route registration", () => { + it("admin-web and filelib-web shells coexist without duplicate routes", async () => { + const app = Fastify({ logger: false }); + await registerStaticSpa(app); + await registerDatabaseSpa(app); + await expect(app.ready()).resolves.toBeDefined(); + await app.close(); + }); + + it("serves the same index.html at /app and /database, and assets under /_filelib", async () => { + const app = Fastify({ logger: false }); + await registerDatabaseSpa(app); + await app.ready(); + + const appShell = await app.inject({ method: "GET", url: "/app" }); + const dbShell = await app.inject({ method: "GET", url: "/database/dashboard/users" }); + + // 前置:构建产物必须存在。缺失时 registerDatabaseSpa 只 warn 不注册路由, + // 断言会全部落到 404 —— 那说明该先跑 `npm run build --prefix filelib-web`, + // 不是测试不适用,所以这里不跳过而是直接失败。 + expect(appShell.statusCode).toBe(200); + expect(dbShell.statusCode).toBe(200); + // 同一份字节:SPA 回退不读请求(ADR-0029)。 + expect(dbShell.body).toBe(appShell.body); + expect(appShell.headers["content-type"]).toContain("text/html"); + // 资源用绝对路径引用(paths.relative=false),否则深层 URL 下会解析错。 + expect(appShell.body).toContain("/_filelib/"); + + await app.close(); + }); +});