From e0e72501801b3d85f96e41cd551ece403742a2bc Mon Sep 17 00:00:00 2001 From: hypercross Date: Sun, 13 Sep 2026 14:24:29 +0800 Subject: [PATCH] fix(dev): proxy content assets to cli server The CLI content server serves the content directory at the URL root, so images and other content assets are requested as root-relative paths (e.g. /adventures/map.png). The dev proxy only forwarded /content, so those requests fell through to historyApiFallback and the browser got index.html instead of the asset. Forward any request that maps to a real file under content/ to the CLI server, keeping /static/ bundles on the web dev server. --- docs/development.md | 9 +++-- rsbuild.config.ts | 64 ++++++++++++++++++++++++++++++----- src/data-loader/file-index.ts | 3 +- 3 files changed, 65 insertions(+), 11 deletions(-) diff --git a/docs/development.md b/docs/development.md index c151846..342b641 100644 --- a/docs/development.md +++ b/docs/development.md @@ -252,8 +252,13 @@ npm test -- --watch - Web 前端:rsbuild 开发服务器,端口 **5173** - CLI 内容服务器:`tttk serve`,端口 **3000**(内容 API + Journal) -rsbuild 会把 `/__CONTENT_INDEX.json`、`/__COMPLETIONS.json`、`/__CONTENT_REGISTRY.json`、`/content`、`/.ttrpg` 代理到 CLI 的 -3000 端口,因此必须先启动 CLI,否则前端拿不到内容。 +rsbuild 会把 `/__CONTENT_INDEX.json`、`/__COMPLETIONS.json`、`/__CONTENT_REGISTRY.json`、`/.ttrpg` +以及内容目录中的静态资源(图片、CSS、CSV 等)代理到 CLI 的 3000 端口,因此必须先启动 CLI, +否则前端拿不到内容。 + +CLI 把内容目录挂在 URL 根路径下(例如 `/adventures/map.png`,**不是** `/content/...`), +所以 rsbuild 会按“请求路径是否对应内容目录中的真实文件”来决定是否代理;`/static/` 下的 +前端构建产物仍由 web dev server 自己提供。 ### Web 开发 diff --git a/rsbuild.config.ts b/rsbuild.config.ts index 5b495c3..f87d60c 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -3,6 +3,43 @@ import { pluginBabel } from "@rsbuild/plugin-babel"; import { pluginSolid } from "@rsbuild/plugin-solid"; import tailwindcss from "@tailwindcss/postcss"; import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer"; +import { statSync } from "node:fs"; +import { join, resolve } from "node:path"; + +// The CLI content server (`npm run dev:cli`, i.e. `tttk serve ./content`) keeps +// port 3000. It serves the content directory at the URL *root*, so content +// assets are requested as root-relative paths (e.g. `/adventures/map.png`), +// not under `/content`. +const CLI_TARGET = "http://localhost:3000"; +const CONTENT_DIR = resolve(process.cwd(), "content"); + +/** + * True when a request targets a real file inside the content directory. + * + * Mirrors the CLI server's static handling: only files are served, so a + * directory path falls through to the SPA fallback. Requests for content + * assets must be proxied to the CLI; otherwise they reach + * `historyApiFallback` and the browser receives `index.html` instead of the + * asset (which is why images failed to render in dev). + */ +function isContentFile(pathname: string): boolean { + // The web dev server owns its own bundled assets under /static/. + if (pathname.startsWith("/static/")) return false; + + let relativePath: string; + try { + relativePath = decodeURIComponent(pathname).replace(/^\/+/, ""); + } catch { + return false; + } + if (!relativePath) return false; + + try { + return statSync(join(CONTENT_DIR, relativePath)).isFile(); + } catch { + return false; + } +} export default defineConfig({ plugins: [ @@ -51,14 +88,25 @@ export default defineConfig({ // Dev web server runs on its own port; the CLI content server keeps // port 3000 (see the proxy targets below). port: 5173, - proxy: { - // Proxy content API requests to the CLI dev server (npm run tttk) - "/__CONTENT_INDEX.json": "http://localhost:3000", - "/__COMPLETIONS.json": "http://localhost:3000", - "/__CONTENT_REGISTRY.json": "http://localhost:3000", - "/content": "http://localhost:3000", - "/.ttrpg": "http://localhost:3000", - }, + proxy: [ + { + // Content API requests served by the CLI dev server (npm run dev:cli). + context: [ + "/__CONTENT_INDEX.json", + "/__COMPLETIONS.json", + "/__CONTENT_REGISTRY.json", + "/.ttrpg", + ], + target: CLI_TARGET, + }, + { + // Content assets (images, css, csv, …) live at the URL root on the CLI + // server, so forward any request that maps to a real file under + // content/ instead of letting it hit the SPA fallback. + context: (pathname: string) => isContentFile(pathname), + target: CLI_TARGET, + }, + ], historyApiFallback: true, }, output: { diff --git a/src/data-loader/file-index.ts b/src/data-loader/file-index.ts index d31c72b..e75176a 100644 --- a/src/data-loader/file-index.ts +++ b/src/data-loader/file-index.ts @@ -8,7 +8,8 @@ * Dev 工作流:运行 CLI 服务器并在 rsbuild 配置中设置代理 * > npm run tttk serve ./content * > npm run dev - * rsbuild 会将 /__CONTENT_INDEX.json、/__COMPLETIONS.json、/__CONTENT_REGISTRY.json、/content/ 代理到 CLI 服务器 + * rsbuild 会将 /__CONTENT_INDEX.json、/__COMPLETIONS.json、/__CONTENT_REGISTRY.json、 + * /.ttrpg 以及内容目录中的静态资源代理到 CLI 服务器 */ import {