Files
fusion/packages/dashboard/vite.config.ts
gsxdsm 4bb3af220e fix(dashboard): mobile keyboard scroll lock, auto-reload hash, ChatView snap
- Add useMobileScrollLock hook using position:fixed body lock (Bootstrap/
  Headless UI/Stripe pattern) to prevent iOS Safari from shifting the
  document and visualViewport when an input inside a fixed-position modal
  is focused. Wire into 15 input-bearing modals plus ChatView, replacing
  ChatView's inline body-overflow effect.
- Widen computeBuildVersion in vite.config.ts to hash the entire app/ tree
  so the version-check poll actually notices rebuilds (FN-3333 follow-up;
  previously only main.tsx and package.json were hashed).
- ChatView: on input blur, suppress keyboard-aware sizing for 450ms while
  reserving mobile-nav-bar space, so the composer snaps to its final
  height in one move instead of crawling down with iOS's keyboard slide
  and then jumping again when the nav bar reappears.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 19:01:10 -07:00

154 lines
4.4 KiB
TypeScript

import { defineConfig, type Plugin } from "vite";
import react from "@vitejs/plugin-react";
import { resolve } from "node:path";
import { writeFileSync, readFileSync, readdirSync, statSync } from "node:fs";
import { execSync } from "node:child_process";
import { createHash } from "node:crypto";
/**
* Generate a deterministic build version string.
*
* Uses the short git commit hash + a content hash of key files so the version
* only changes when the actual source (or uncommitted changes to those files)
* changes. Falls back to package.json version when git is unavailable.
*/
function computeBuildVersion(): string {
// Get git short hash or fall back to package.json version
let prefix: string;
try {
prefix = execSync("git rev-parse --short HEAD", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
} catch {
try {
const pkg = JSON.parse(readFileSync(resolve(__dirname, "package.json"), "utf-8"));
prefix = typeof pkg.version === "string" ? pkg.version : "0.0.0";
} catch {
prefix = "0.0.0";
}
}
// Content hash of the entire app/ source tree + package.json. Hashing only
// a couple of entry files (the previous behavior) meant that edits to any
// other component or stylesheet produced an identical build version, so the
// dashboard's version-check poll never noticed the new bundle and the
// "reload available" prompt never fired (FN-3333 follow-up).
const hasher = createHash("sha1");
const appDir = resolve(__dirname, "app");
// Collect, sort, then hash so the order is stable across platforms and runs.
const files: string[] = [];
const walk = (dir: string): void => {
let entries: string[];
try {
entries = readdirSync(dir);
} catch {
return;
}
for (const entry of entries) {
if (entry === "node_modules" || entry === "__tests__" || entry.startsWith(".")) continue;
const full = resolve(dir, entry);
let info: ReturnType<typeof statSync>;
try {
info = statSync(full);
} catch {
continue;
}
if (info.isDirectory()) {
walk(full);
} else if (info.isFile()) {
files.push(full);
}
}
};
walk(appDir);
files.sort();
for (const f of files) {
try {
hasher.update(f.slice(appDir.length));
hasher.update(readFileSync(f));
} catch {
// file may have been deleted between readdir and read — skip
}
}
try {
hasher.update(readFileSync(resolve(__dirname, "package.json")));
} catch {
// ignore
}
const contentHash = hasher.digest("hex").slice(0, 8);
return `${prefix}-${contentHash}`;
}
const buildVersion = computeBuildVersion();
function emitVersionJson(): Plugin {
return {
name: "fusion-emit-version-json",
apply: "build",
closeBundle() {
const outFile = resolve(__dirname, "dist/client/version.json");
writeFileSync(outFile, `${JSON.stringify({ version: buildVersion })}\n`);
console.log(`[fusion] build version: ${buildVersion}`);
},
};
}
export default defineConfig({
root: "app",
plugins: [react(), emitVersionJson()],
define: {
__BUILD_VERSION__: JSON.stringify(buildVersion),
},
resolve: {
alias: {
"@fusion/core": resolve(__dirname, "../core/src/types.ts"),
},
},
optimizeDeps: {
include: [
"@xterm/xterm",
"@xterm/addon-fit",
"@xterm/addon-web-links",
"@xterm/addon-webgl",
],
},
build: {
outDir: "../dist/client",
emptyOutDir: true,
target: "es2022",
cssCodeSplit: true,
sourcemap: false,
assetsInlineLimit: 4096,
rollupOptions: {
output: {
entryFileNames: "assets/[name]-[hash].js",
chunkFileNames: "assets/[name]-[hash].js",
assetFileNames: "assets/[name]-[hash][extname]",
manualChunks: (id) => {
if (id.includes("/node_modules/react/") || id.includes("/node_modules/react-dom/")) {
return "vendor-react";
}
if (id.includes("/node_modules/@xterm/xterm/")) {
return "vendor-xterm";
}
if (id.includes("/node_modules/@codemirror/")) {
return "vendor-codemirror";
}
return undefined;
},
},
},
},
server: {
proxy: {
"/api": {
target: `http://localhost:${process.env.FUSION_API_PORT ?? "4040"}`,
changeOrigin: true,
ws: true,
},
},
},
});