Files
AgentHub/client/vite.config.ts
T
nntrivi2001 e3745d7504 fix(build): target ES2021 so esbuild keeps xterm 6's requestMode variable
Vite's es2020 default made esbuild 0.25 lower `let r; …(r||={})` to
`void 0||(i={})` without the declaration, so every DECRQM query tmux sends
threw "i is not defined" inside the terminal's parser. Found by a headless
smoke run of the preview build.
2026-10-26 14:43:01 +07:00

122 lines
4.8 KiB
TypeScript

/**
* @file vite.config.ts
* @description Vite build and dev-server configuration for the dashboard client — React plugin, an API/WebSocket proxy that honours DASHBOARD_PORT, build-time injection of the project version as `__APP_VERSION__`, one chunk per UI language, hidden source maps, and pre-compressed `<file>.br`/`<file>.gz` siblings of every text asset ≥ 1 KB (served by the server for `/assets/`).
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { brotliCompressSync, constants, gzipSync } from "node:zlib";
import { defineConfig, type Plugin } from "vite";
import react from "@vitejs/plugin-react";
// The dashboard's displayed version is the canonical project version from the
// repo-root package.json (the version CI cuts releases from), injected at build
// time as the `__APP_VERSION__` global so the UI footer always shows the real
// version instead of a hardcoded string. Vite runs from the client dir, so the
// root manifest is normally one level up; fall back to the client manifest, and
// finally a placeholder, so the build never fails when the root file is absent
// (e.g. a Docker stage that only copies client/). The global is declared in
// `client/src/vite-env.d.ts`.
function resolveAppVersion(): string {
for (const rel of ["../package.json", "package.json"]) {
try {
const { version } = JSON.parse(readFileSync(resolve(process.cwd(), rel), "utf8"));
if (version) return version as string;
} catch {
// Not found or unreadable at this path — try the next candidate.
}
}
return "0.0.0";
}
const APP_VERSION = resolveAppVersion();
// Honour DASHBOARD_PORT so the proxy follows when `npm run dev:server` is
// moved off the default 4820 (e.g. when an SSH `LocalForward` already holds
// 4820 on `127.0.0.1` and `::1`). The dev server reads the same env var from
// `server/index.js`, so a single `DASHBOARD_PORT=4821 npm run dev` keeps
// both sides in lockstep.
//
// We also target `127.0.0.1` rather than `localhost`: when several listeners
// exist on the same port across IP families (loopback-specific SSH binds vs.
// Node's wildcard listen), macOS routes connections by socket specificity,
// so `localhost` can resolve into the wrong process. An explicit IPv4 loopback
// is what the embedded server in production binds to anyway.
const DASHBOARD_PORT = parseInt(process.env.DASHBOARD_PORT || "4820", 10);
// Writes `<file>.br` (quality 11) and `<file>.gz` (level 9) next to every text
// asset of 1 KB or more once the build is on disk, so the server can send the
// smallest encoding without compressing per request. node:zlib only.
function precompress(): Plugin {
let outDir = "dist";
const walk = (dir: string): string[] =>
readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
e.isDirectory() ? walk(join(dir, e.name)) : [join(dir, e.name)]
);
return {
name: "agenthub-precompress",
apply: "build",
configResolved(c) {
outDir = resolve(c.root, c.build.outDir);
},
closeBundle() {
for (const file of walk(outDir)) {
if (!/\.(js|css|svg|json|html)$/.test(file)) continue;
const buf = readFileSync(file);
if (buf.length < 1024) continue;
writeFileSync(
`${file}.br`,
brotliCompressSync(buf, {
params: {
[constants.BROTLI_PARAM_QUALITY]: 11,
[constants.BROTLI_PARAM_SIZE_HINT]: buf.length,
},
})
);
writeFileSync(`${file}.gz`, gzipSync(buf, { level: 9 }));
}
},
};
}
export default defineConfig({
plugins: [react(), precompress()],
define: {
__APP_VERSION__: JSON.stringify(APP_VERSION),
},
server: {
port: 5173,
proxy: {
"/api": {
target: `http://127.0.0.1:${DASHBOARD_PORT}`,
changeOrigin: true,
},
"/ws": {
target: `ws://127.0.0.1:${DASHBOARD_PORT}`,
ws: true,
},
},
},
build: {
outDir: "dist",
// ES2021 or newer, never Vite's es2020 default: esbuild 0.25 lowers
// xterm 6's `let r; …(r||={})` (requestMode, DECRQM) to `void 0||(i={})`
// and drops the `let`, so every DECRQM query tmux sends threw
// "i is not defined" inside the terminal's parser. React 19 needs these
// browsers anyway.
target: ["es2021", "chrome100", "edge100", "firefox100", "safari15"],
// Maps are still written (for debugging a build), but no bundle points at
// them, so a browser never downloads them.
sourcemap: "hidden",
rollupOptions: {
output: {
// i18n/index.ts loads locale files lazily: one chunk per language.
manualChunks(id) {
const m = /[\\/]i18n[\\/]locales[\\/](\w+)[\\/]/.exec(id);
return m ? `locale-${m[1]}` : undefined;
},
},
},
},
});