- 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>
154 lines
4.4 KiB
TypeScript
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,
|
|
},
|
|
},
|
|
},
|
|
});
|