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", }, }, });