import { defineConfig, type PluginOption } from "vite"; import react from "@vitejs/plugin-react"; import { sentryVitePlugin } from "@sentry/vite-plugin"; import { readFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { nodePolyfills } from "vite-plugin-node-polyfills"; const host = process.env.TAURI_DEV_HOST; // Optional override so parallel `dev:app:win` runs across worktrees can // avoid the hardcoded 1420 collision. Default 1420 preserves prior behavior; // HMR companion port is dev port + 1 (used only when TAURI_DEV_HOST is set). const devPort = Number(process.env.OPENHUMAN_DEV_PORT) || 1420; const hmrPort = devPort + 1; const __dirname = dirname(fileURLToPath(import.meta.url)); const pkg = JSON.parse( readFileSync(resolve(__dirname, "package.json"), "utf8"), ) as { version: string }; // Canonical Sentry release — must stay in sync with the string produced by // `SENTRY_RELEASE` in app/src/utils/config.ts and the core sidecar's // `sentry::init` in src/main.rs so events from every surface group together. function computeSentryRelease(): string { const raw = (process.env.SENTRY_RELEASE ?? "").trim(); if (raw) return raw; const sha = (process.env.VITE_BUILD_SHA ?? "").trim().slice(0, 12); return sha ? `openhuman@${pkg.version}+${sha}` : `openhuman@${pkg.version}`; } // Gate source-map upload on the presence of SENTRY_AUTH_TOKEN so local dev // and CI jobs that don't ship to users skip the plugin silently. The // companion `SENTRY_ORG` / `SENTRY_PROJECT` come from CI env. function maybeSentryPlugin(): PluginOption | null { const authToken = process.env.SENTRY_AUTH_TOKEN; if (!authToken) return null; return sentryVitePlugin({ authToken, // Self-hosted Sentry — without `url`, the plugin defaults to sentry.io // and silently no-ops the upload. Falls back to sentry.io if unset for // local builds against the SaaS instance. url: process.env.SENTRY_URL, org: process.env.SENTRY_ORG, project: process.env.SENTRY_PROJECT, release: { name: computeSentryRelease(), // The frontend already passes this release to Sentry.init(). Keeping the // plugin's virtual release module enabled can be transformed by the node // polyfill injector into startup code that calls Rollup helpers before // they are initialized in the generated desktop bundle. inject: false, }, sourcemaps: { // Vite emits hashed asset files into `app/dist/assets/`. Upload every // .js / .map the build produces. // // `assets` is resolved by sentry-vite-plugin against `process.cwd()`, // not the Vite `root` — so a relative path like `../dist/**` would // miss when `pnpm tauri build` runs with cwd=`app/` and silently emit // `Didn't find any matching sources for debug ID upload`. Use absolute // paths anchored at this config file's directory (`app/`) to be // immune to whatever cwd the parent process sets. assets: [ resolve(__dirname, "dist/**/*.js"), resolve(__dirname, "dist/**/*.map"), ], // Never ship raw .map files to end users; the upload keeps a copy // server-side for symbolication while the bundled app strips them. filesToDeleteAfterUpload: [resolve(__dirname, "dist/**/*.map")], }, // Release tagging + commits are handled by sentry-cli / the plugin // itself when AUTH_TOKEN and CI env (GITHUB_SHA etc.) are present. telemetry: false, }); } function guardCefRelListSupportsPlugin(): PluginOption { return { name: "openhuman:guard-cef-rel-list-supports", enforce: "post", renderChunk(code) { const unsafe = 'relList && relList.supports && relList.supports("modulepreload")'; const guarded = 'relList && typeof relList.supports === "function" && relList.supports("modulepreload")'; const next = code.split(unsafe).join(guarded); return next === code ? null : { code: next, map: null }; }, }; } // `VITE_OPENHUMAN_TARGET=web` switches the build to the browser-hosted // flavor: output lands in `dist-web/` so the desktop build artifact in // `dist/` (consumed by `cargo tauri build`) is never clobbered, and the // `import.meta.env.VITE_OPENHUMAN_TARGET` value is exposed to runtime code // that wants a build-time signal in addition to the runtime `isTauri()` // check. Default (`undefined` / `desktop`) keeps the historical behavior. const buildTarget = (process.env.VITE_OPENHUMAN_TARGET ?? "desktop").trim(); const isWebTarget = buildTarget === "web"; // https://vite.dev/config/ export default defineConfig(async () => ({ root: "src", publicDir: "../public", // Read env files from the repo root (not `app/src/`, which is the vite // `root` and would be the default `envDir`). Lets `pnpm dev:app` pick up // `VITE_BACKEND_URL` / `VITE_OPENHUMAN_APP_ENV` from the same root `.env` // the Rust shell uses, instead of needing a separate `app/.env.local`. // Without this, `import.meta.env.VITE_*` is empty in dev (Vite does not // inherit `process.env` for VITE_-prefixed vars), so `BACKEND_URL` falls // through to the production fallback in `src/utils/config.ts` even when // the shell exports staging URLs. envDir: resolve(__dirname, ".."), build: { outDir: isWebTarget ? "../dist-web" : "../dist", emptyOutDir: true, // Desktop CEF has surfaced a runtime where `link.relList.supports` is // truthy but not callable. Vite calls it both in the modulepreload // polyfill and the dynamic-import preload helper, before React mounts. modulePreload: false, // Emit source maps so @sentry/vite-plugin can upload them; the plugin // deletes the on-disk .map files after upload so users don't receive // them in the shipped bundle. sourcemap: true, }, plugins: [ nodePolyfills({ include: ["buffer", "process", "util", "os", "crypto", "stream"], globals: { Buffer: true, process: true, global: true, }, }), guardCefRelListSupportsPlugin(), react(), maybeSentryPlugin(), ].filter(Boolean) as PluginOption[], // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // // 1. prevent Vite from obscuring rust errors clearScreen: false, // 2. tauri expects a fixed port, fail if that port is not available server: { port: devPort, strictPort: true, // `false` lets Vite pick its own loopback default; on Windows that lands // on `::1` only, leaving 127.0.0.1 unbound. The Tauri dev-server proxy // (vendored tauri-cef, reqwest under the hood) resolves `localhost` and // can pick either stack — when it picks 127.0.0.1 the request fails, // which surfaces as a blank webview / white screen because the SPA // bundle never loads. `true` maps to `server.listen('0.0.0.0')` in Vite, // binding **every network adapter** (loopback + LAN) so whichever stack // reqwest's DNS picks for `localhost` has a listener. Side effect: the // dev HMR websocket and bundled sources are reachable from other // machines on the same network — fine for `tauri dev`, but on a shared // or corporate Wi-Fi consider overriding with `host: 'localhost'` (and // accepting the dual-stack hazard) instead. Production builds are // unaffected. host: host || true, allowedHosts: [ "frontend-runner-openhuman-git-main-vezuresxyz.vercel.app", ], hmr: host ? { protocol: "ws", host, port: hmrPort, } : { // Tauri CEF loads the app from tauri.localhost; without this the // HMR client tries ws://tauri.localhost/ and gets ERR_CONNECTION_REFUSED. // Force the client to connect to the Vite dev server directly. protocol: "ws", host: "localhost", port: devPort, clientPort: devPort, }, watch: { // 3. tell Vite to ignore watching `src-tauri` directory (includes src-tauri/ai) ignored: ["**/src-tauri/**"], }, }, resolve: { alias: { buffer: "buffer", process: "process/browser", util: "util", os: "os-browserify/browser", }, }, optimizeDeps: { include: ["buffer", "process", "util", "os-browserify"], }, }));