Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .dev.vars.example
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,41 @@ ADMIN_PASS=
# 留空表示仅允许同源请求。
ALLOW_URLS=

# 前端静态资源 CDN 地址(可选)。需指向包含完整前端 dist 的目录(含 index.html)。
# 配置后浏览器直连 CDN 加载 JS/CSS/图片等资源,不再经 Worker 中转。
# 注入自动在两条路径中选择:
# A. CDN 上存在本地构建引用的哈希资产 → 直接下发本地 index.html 并注入 cdn
# (不要求 CDN 能返回 HTML —— npmmirror 等禁止访问 .html 的 CDN 靠这条工作,
# 等价 Go Release 版行为)
# B. 否则改用 CDN 自己的 index.html,保证 HTML 与哈希资产同源一致
# A、B 都不可用 → 不注入,资源回退源站加载(不会白屏)
# A 要求 CDN 上的产物与本次部署的 dist 是同一份构建(哈希逐字节相同)。默认
# `pnpm run build` 会从 npm 拉取【已发布】的前端 dist(scripts/fetch-frontend.mjs),
# 与 CDN 内容天然同源,故 A 会命中 —— npmmirror 这类禁止访问 .html 的 CDN 只能
# 靠 A 工作;若用 FRONTEND_BUILD_FROM_SOURCE=1 从 main 现构建,哈希与 CDN 不一致,
# A 必然落空(只能退化到 B,而 B 对 npmmirror 不可用)。
# 支持 $version 占位符,替换为本构建的前端版本号(构建期写入,解析失败回退 latest)。
# 注意:Cloudflare 默认「资源优先」,/ 与 /index.html 会被静态层直接返回而绕过
# Worker,注入不会生效;本仓库 wrangler.jsonc 已用 assets.run_worker_first 放行
# 这两个 HTML 入口(带 hash 的 JS/CSS 仍由静态层直出,不经 Worker)。
#
# ⚠️ 资源由浏览器直接加载,CDN 必须对 .js/.css/字体返回 Access-Control-Allow-Origin
# (前端产物以 crossorigin 加载 module script / stylesheet)。
# jsdelivr、unpkg 固定返回 *;npmmirror 在请求带 Origin 时回显该头,同样可用。
#
# ⚠️ 行为变更(迁移说明):旧实现是 assetsRouter 的 /:folder/:filepath* 路由,把
# 每个静态资源请求都 302 重定向到 CDN。该路由已移除 —— 它取不到 filepath,
# 且在静态层优先的平台上根本不会进 Worker;现在统一改为上面的 HTML 注入,
# 由浏览器直连 CDN。未配置 ASSET_URLS 时静态资源一律由源站提供。
# 另:/assets、/images、/streamer、/static 下的文件若在源站确实缺失,仍会 302
# 到 CDN(对齐 Go 版 static.go 的 folders),避免 SPA 兜底把 index.html 当作
# .js/.css 返回。
# 例:
# https://registry.npmmirror.com/@openlist-frontend/openlist-frontend/$version/files/dist
# https://cdn.jsdelivr.net/npm/@openlist-frontend/openlist-frontend@$version/dist
# https://unpkg.com/@openlist-frontend/openlist-frontend@$version/dist
ASSET_URLS=

# MySQL 连接串(DB_DRIVER=mysql 时必填),如 mysql://user:pass@host:3306/db
# 也可改用 wrangler.jsonc 中注释的 MYSQL_HOST / MYSQL_PORT / MYSQL_USER / MYSQL_PASS / MYSQL_NAME 分项。
MYSQL_URLS=
Expand Down
35 changes: 35 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,41 @@ ADMIN_PASS=
# 留空表示仅允许同源请求。
ALLOW_URLS=

# 前端静态资源 CDN 地址(可选)。需指向包含完整前端 dist 的目录(含 index.html)。
# 配置后浏览器直连 CDN 加载 JS/CSS/图片等资源,不再经 Worker 中转。
# 注入自动在两条路径中选择:
# A. CDN 上存在本地构建引用的哈希资产 → 直接下发本地 index.html 并注入 cdn
# (不要求 CDN 能返回 HTML —— npmmirror 等禁止访问 .html 的 CDN 靠这条工作,
# 等价 Go Release 版行为)
# B. 否则改用 CDN 自己的 index.html,保证 HTML 与哈希资产同源一致
# A、B 都不可用 → 不注入,资源回退源站加载(不会白屏)
# A 要求 CDN 上的产物与本次部署的 dist 是同一份构建(哈希逐字节相同)。默认
# `pnpm run build` 会从 npm 拉取【已发布】的前端 dist(scripts/fetch-frontend.mjs),
# 与 CDN 内容天然同源,故 A 会命中 —— npmmirror 这类禁止访问 .html 的 CDN 只能
# 靠 A 工作;若用 FRONTEND_BUILD_FROM_SOURCE=1 从 main 现构建,哈希与 CDN 不一致,
# A 必然落空(只能退化到 B,而 B 对 npmmirror 不可用)。
# 支持 $version 占位符,替换为本构建的前端版本号(构建期写入,解析失败回退 latest)。
# 注意:注入只发生在「HTML 入口请求能到达 Worker」时。若平台默认用静态层直出
# /index.html(如 Cloudflare 的资源优先路由、EdgeOne middleware rewrite),
# 需让 / 与 /index.html 走 Worker(CF 见 wrangler.jsonc 的 run_worker_first)。
#
# ⚠️ 资源由浏览器直接加载,CDN 必须对 .js/.css/字体返回 Access-Control-Allow-Origin
# (前端产物以 crossorigin 加载 module script / stylesheet)。
# jsdelivr、unpkg 固定返回 *;npmmirror 在请求带 Origin 时回显该头,同样可用。
#
# ⚠️ 行为变更(迁移说明):旧实现是 assetsRouter 的 /:folder/:filepath* 路由,把
# 每个静态资源请求都 302 重定向到 CDN。该路由已移除 —— 它取不到 filepath,
# 且在静态层优先的平台上根本不会进 Worker;现在统一改为上面的 HTML 注入,
# 由浏览器直连 CDN。未配置 ASSET_URLS 时静态资源一律由源站提供。
# 另:/assets、/images、/streamer、/static 下的文件若在源站确实缺失,仍会 302
# 到 CDN(对齐 Go 版 static.go 的 folders),避免 SPA 兜底把 index.html 当作
# .js/.css 返回。
# 例:
# https://registry.npmmirror.com/@openlist-frontend/openlist-frontend/$version/files/dist
# https://cdn.jsdelivr.net/npm/@openlist-frontend/openlist-frontend@$version/dist
# https://unpkg.com/@openlist-frontend/openlist-frontend@$version/dist
ASSET_URLS=

# MySQL 连接串(DB_DRIVER=mysql 时必填),如 mysql://user:pass@host:3306/db
# 也可改用 wrangler.jsonc 中注释的 MYSQL_HOST / MYSQL_PORT / MYSQL_USER / MYSQL_PASS / MYSQL_NAME 分项。
MYSQL_URLS=
Expand Down
234 changes: 117 additions & 117 deletions cloud-functions/[[default]].js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions scripts/_regress.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,11 @@ try {
check("代理 save/load 正常", ld?.users?.[0]?.username === "admin")

console.log("=== H. 配置错误透出 ===")
const ce1 = await backendMod.getStoreConfigError({ __requestOrigin: origin })
const ce1 = (await backendMod.getStoreConfigErrorDetail({ __requestOrigin: origin })).message
check("worker 无存储错误", typeof ce1 === "string" && ce1.includes("No storage backend"))
const ce2 = await backendMod.getStoreConfigError({ DB_DRIVER: "kv", KV: respClient })
const ce2 = (await backendMod.getStoreConfigErrorDetail({ DB_DRIVER: "kv", KV: respClient })).message
check("KV 缺密钥错误(精确)", typeof ce2 === "string" && ce2.includes("JWT_SECRET"), String(ce2).slice(0, 50))
const ce3 = await backendMod.getStoreConfigError({ DB_DRIVER: "kv", KV: respClient, JWT_SECRET, __requestOrigin: origin })
const ce3 = (await backendMod.getStoreConfigErrorDetail({ DB_DRIVER: "kv", KV: respClient, JWT_SECRET, __requestOrigin: origin })).message
check("配置完整 → null", ce3 === null)

console.log("=== I. 绑定形态 ===")
Expand Down
119 changes: 115 additions & 4 deletions scripts/fetch-frontend.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,25 @@
* 1. FRONTEND_DIST 环境变量:已构建好的 dist 目录路径(最快,CI 缓存场景)
* 2. FRONTEND_REPO 环境变量:本地官方前端仓库路径(自动 install + build)
* 3. 同级目录 ../OpenList-Frontend(monorepo 布局,自动探测,自动 install + build)
* 4. 默认:从 Git 克隆官方仓库并构建
* 4. 默认:下载 npm 上【已发布】的 dist(版本取 registry 的 latest,
* 可用 FRONTEND_VERSION 固定)
* 5. FRONTEND_BUILD_FROM_SOURCE=1:从 Git 克隆前端 main 分支并现构建
*
* 为什么默认取「已发布 dist」而不是「克隆 main 现构建」:
* 前端产物是内容哈希文件名(/assets/index-XXXX.js),而 CDN(jsdelivr /
* unpkg / npmmirror)提供的正是 npm 包里那一份 dist。若本地从 main 现构建,
* 哈希与 CDN 上的不一致,ASSET_URLS 的「路径 A」(下发本地 index.html +
* CDN 资产,见 src/backend/server/assets.ts)HEAD 探测必然失败,只能退化到
* 拉 CDN 的 index.html;而 npmmirror 等镜像禁止访问 .html(451),于是 CDN
* 完全用不了。取已发布 dist 可让两边哈希天然同源:路径 A 命中,npmmirror
* 可用(等价 Go Release 版行为)。同时 stampFrontendVersion 会把该版本号写进
* index.html,使 ASSET_URLS 的 $version 正好解析到这份 dist 对应的版本。
*
* 用法:
* FRONTEND_DIST=/path/to/dist node scripts/fetch-frontend.mjs
* FRONTEND_REPO=../OpenList-Frontend node scripts/fetch-frontend.mjs
* FRONTEND_VERSION=4.2.6 node scripts/fetch-frontend.mjs
* FRONTEND_BUILD_FROM_SOURCE=1 node scripts/fetch-frontend.mjs
* node scripts/fetch-frontend.mjs
*/

Expand All @@ -32,6 +46,13 @@ const OFFICIAL_REPO_URL =
"https://github.com/OpenListTeam/OpenList-Frontend.git"
const OFFICIAL_REPO_REF = process.env.FRONTEND_GIT_REF || "main"

// 已发布 dist 的来源(默认路径)。ASSET_URLS 指向的 CDN 提供的正是这份 npm
// 包内容,取它才能保证内容哈希与 CDN 同源(见文件头说明)。
const REGISTRY_URL =
process.env.FRONTEND_REGISTRY || "https://registry.npmjs.org"
const PKG_NAME =
process.env.FRONTEND_PKG || "@openlist-frontend/openlist-frontend"

// 多语言翻译包:官方前端仓库不提交非英文翻译(由 Crowdin 维护),随 release 发布。
// 直接 pnpm build 只会得到英文界面,因此 CF/EO 构建时需在此拉取后再构建。
const I18N_TAR_URL =
Expand Down Expand Up @@ -78,9 +99,43 @@ function replaceDist(src) {
console.log(` Copying frontend dist: ${src} -> ${DEST}`)
fs.rmSync(DEST, { recursive: true, force: true })
fs.cpSync(src, DEST, { recursive: true })
stampFrontendVersion(src)
console.log(`✓ Frontend dist ready (${DEST})`)
}

/**
* 构建期戳:把前端版本号写入 dist/index.html 的 <meta name="frontend-version">。
*
* 运行时 ASSET_URLS 的 $version 占位符优先从这里取值——版本与本次构建的 dist
* 同源产生,保证 CDN 地址指向的版本与实际部署的前端一致(否则哈希资产会 404)。
* 前端仓库不存在(如仅提供预构建 dist)时跳过,运行时回退 latest。
*/
function stampFrontendVersion(src) {
try {
const pkg = JSON.parse(
fs.readFileSync(path.join(path.resolve(src, ".."), "package.json"), "utf-8"),
)
// 只信任官方前端包的版本号:FRONTEND_DIST 可能指向任意目录,
// 误读(例如 worker 自身 package.json 的 4.2.3)会戳出错误的 CDN 版本。
if (!/openlist-frontend/i.test(pkg?.name || "")) return
const version = pkg?.version
if (!version) return
const idx = path.join(DEST, "index.html")
let html = fs.readFileSync(idx, "utf-8")
if (/name=["']frontend-version["']/.test(html)) return
html = html.replace(
/<head([^>]*)>/i,
`<head$1>\n <meta name="frontend-version" content="${version}">`,
)
fs.writeFileSync(idx, html)
console.log(` Stamped frontend-version ${version} into dist/index.html`)
} catch (err) {
console.warn(
` [fetch-frontend] stamp frontend-version skipped: ${err?.message || err}`,
)
}
}

/**
* 拉取官方前端发布的多语言翻译包,解压到前端仓库 src/lang/ 后运行
* i18n.mjs 补齐 entry.ts 与缺失翻译,保证构建产物包含完整多语言。
Expand Down Expand Up @@ -142,7 +197,55 @@ function buildLocalRepo(repo) {
replaceDist(path.join(abs, "dist"))
}

function main() {
/**
* 下载 npm 上【已发布】的前端 dist。
*
* 版本取 FRONTEND_VERSION,未设置时取 registry 的 latest dist-tag。tarball 里
* 只解出 package/dist 与 package/package.json —— 后者供 stampFrontendVersion
* 读出真实发布版本号(解出 LICENSE/README 没有意义)。
*
* 失败时直接抛错终止构建,不静默回退到「克隆 main 现构建」:那条路产出的哈希
* 与 CDN 不一致,会让 ASSET_URLS 的路径 A 悄悄失效(npmmirror 直接不可用)。
* 确实需要现构建时显式设置 FRONTEND_BUILD_FROM_SOURCE=1。
*/
async function fetchPublishedDist() {
console.log(` Querying registry: ${REGISTRY_URL} (${PKG_NAME})`)
const res = await fetch(`${REGISTRY_URL}/${PKG_NAME}`, {
headers: { accept: "application/json" },
signal: AbortSignal.timeout(30_000),
})
if (!res.ok) {
throw new Error(`registry metadata request failed: HTTP ${res.status}`)
}
const meta = await res.json()
const version = process.env.FRONTEND_VERSION || meta?.["dist-tags"]?.latest
if (!version) {
throw new Error(
`cannot determine version: no dist-tags.latest for ${PKG_NAME}`,
)
}
const tarball = meta?.versions?.[version]?.dist?.tarball
if (!tarball) {
throw new Error(`version ${version} is not published for ${PKG_NAME}`)
}

const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openlist-frontend-npm-"))
const tgz = path.join(tmp, "pkg.tgz")
try {
console.log(` Downloading published dist: ${PKG_NAME}@${version}`)
run(`curl -fL --retry 3 -o "${tgz}" "${tarball}"`)
// 以 tmp 为 cwd、用相对路径解包:Windows 上 tar 会把 "C:\\..." 里的盘符冒号
// 当成远程主机("Cannot connect to C: resolve failed"),不能把绝对路径喂给 tar。
run(`tar -xzf pkg.tgz package/dist package/package.json`, { cwd: tmp })
const src = path.join(tmp, "package", "dist")
requireDist(src)
replaceDist(src)
} finally {
fs.rmSync(tmp, { recursive: true, force: true })
}
}

async function main() {
console.log("[fetch-frontend] Fetching official frontend build artifacts...")

// 1. 本地已构建产物目录(显式指定)
Expand All @@ -169,7 +272,15 @@ function main() {
return
}

// 4. 从 Git 克隆并构建(默认兜底)
// 4. 下载 npm 上已发布的 dist(默认)
// 从 main 现构建的产物哈希与 CDN 不一致,会让路径 A 失效、npmmirror 之类的
// 镜像完全不可用(详见文件头),故默认改为取已发布产物。
if (process.env.FRONTEND_BUILD_FROM_SOURCE !== "1") {
await fetchPublishedDist()
return
}

// 5. 从 Git 克隆 main 并构建(FRONTEND_BUILD_FROM_SOURCE=1 时使用)
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openlist-frontend-"))
console.log(` Cloning official frontend: ${OFFICIAL_REPO_URL}#${OFFICIAL_REPO_REF}`)
try {
Expand All @@ -186,7 +297,7 @@ function main() {
}

try {
main()
await main()
} catch (err) {
console.error("[fetch-frontend] Failed:", err?.message || err)
process.exit(1)
Expand Down
Loading
Loading