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.
118 lines
3.4 KiB
TypeScript
118 lines
3.4 KiB
TypeScript
import { defineConfig } from "@rsbuild/core";
|
|
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: [
|
|
pluginBabel({
|
|
include: /\.(?:jsx|tsx)$/,
|
|
}),
|
|
pluginSolid(),
|
|
],
|
|
tools: {
|
|
postcss: {
|
|
postcssOptions: {
|
|
plugins: [tailwindcss()],
|
|
},
|
|
},
|
|
rspack: {
|
|
resolve: {
|
|
// Allow TypeScript sources to use explicit `.js` specifiers (required
|
|
// by the CLI's NodeNext build) while the bundler resolves them to .ts.
|
|
extensionAlias: {
|
|
".js": [".ts", ".tsx", ".js"],
|
|
},
|
|
},
|
|
module: {
|
|
rules: [
|
|
{
|
|
// Built-in doc entries are statically imported as strings
|
|
test: /[\\/]doc-entries[\\/].*\.md$/,
|
|
type: "asset/source",
|
|
},
|
|
],
|
|
},
|
|
plugins: [
|
|
process.env.ANALYZE ? new BundleAnalyzerPlugin() : undefined,
|
|
].filter(Boolean),
|
|
},
|
|
},
|
|
html: {
|
|
template: "./src/index.html",
|
|
},
|
|
source: {
|
|
entry: {
|
|
index: "./src/main.tsx",
|
|
},
|
|
},
|
|
server: {
|
|
// Dev web server runs on its own port; the CLI content server keeps
|
|
// port 3000 (see the proxy targets below).
|
|
port: 5173,
|
|
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: {
|
|
distPath: {
|
|
root: "dist/web",
|
|
},
|
|
},
|
|
});
|