import { execFileSync } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import os from "node:os"; import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; const portValue = Number.parseInt(process.env.PORT ?? "", 10); const devPort = Number.isFinite(portValue) && portValue > 0 ? portValue : 5173; const allowedHosts = new Set(); const envAllowedHosts = process.env.VITE_ALLOWED_HOSTS ?? ""; const addHost = (value?: string | null) => { const trimmed = value?.trim(); if (!trimmed) return; allowedHosts.add(trimmed); }; envAllowedHosts.split(",").forEach(addHost); addHost(process.env.OPENWORK_PUBLIC_HOST ?? null); const hostname = os.hostname(); addHost(hostname); const shortHostname = hostname.split(".")[0]; if (shortHostname && shortHostname !== hostname) { addHost(shortHostname); } const appRoot = resolve(fileURLToPath(new URL(".", import.meta.url))); const appPackagePath = resolve(appRoot, "package.json"); const desktopPackagePath = resolve(appRoot, "..", "desktop", "package.json"); function firstNonEmpty(values: Array): string | null { for (const value of values) { const trimmed = value?.trim(); if (trimmed) return trimmed; } return null; } function readLocalGitSha(): string | null { try { const output = execFileSync("git", ["rev-parse", "HEAD"], { cwd: appRoot, encoding: "utf8", stdio: "pipe", }); return output.trim() || null; } catch { return null; } } function readPackageVersion(packagePath: string): string | null { if (!existsSync(packagePath)) return null; const parsed = JSON.parse(readFileSync(packagePath, "utf8")) as { version?: string }; return parsed.version?.trim() || null; } const buildAppVersion = process.env.VITE_OPENWORK_APP_VERSION?.trim() || readPackageVersion(desktopPackagePath) || readPackageVersion(appPackagePath) || "0.0.0"; const buildSha = firstNonEmpty([ process.env.VITE_OPENWORK_BUILD_SHA, process.env.OPENWORK_GIT_SHA, process.env.GITHUB_SHA, ]) ?? readLocalGitSha(); const shortBuildSha = buildSha ? buildSha.slice(0, 7) : ""; // Load the Tauri → Electron migration-release fragment if present. Written // by scripts/migration/01-cut-migration-release.mjs for the specific // release commit; absent otherwise so every other build has the migration // prompt dormant. Pre-parsed here so Vite's define/import.meta.env picks // up the keys without a custom plugin. function loadMigrationReleaseEnv(): Record { const fragmentPath = resolve(appRoot, ".env.migration-release"); if (!existsSync(fragmentPath)) return {}; const out: Record = {}; const raw = readFileSync(fragmentPath, "utf8"); for (const line of raw.split(/\r?\n/)) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; const eq = trimmed.search("="); if (eq < 0) continue; const key = trimmed.slice(0, eq).trim(); if (!key.startsWith("VITE_")) continue; out[key] = trimmed.slice(eq + 1).trim(); } return out; } const migrationReleaseEnv = loadMigrationReleaseEnv(); // Electron packaged builds load index.html via `file://`, so asset URLs // must be relative. Tauri serves via its own protocol so absolute paths // work there. Gate on an env var the electron build script sets. const isElectronPackagedBuild = process.env.OPENWORK_ELECTRON_BUILD === "1"; // Headless-web dev (scripts/dev-headless-web.ts): serve /api/den same-origin // from the dev server, proxied to the Den control plane, so the browser never // issues cross-origin Den API calls (Den does not serve CORS). The app is // pointed here via VITE_DEN_API_BASE_URL; sign-in still opens the real Den // web app. Inert unless the launcher sets the target env. No gateway marker: // that runtime implies a provisioned cloud instance, which local dev lacks. const headlessDenTarget = (process.env.OPENWORK_DEV_HEADLESS_DEN_TARGET ?? "").trim(); export default defineConfig({ base: isElectronPackagedBuild ? "./" : "/", define: { ...Object.fromEntries( Object.entries(migrationReleaseEnv).map(([k, v]) => [ `import.meta.env.${k}`, JSON.stringify(v), ]), ), "import.meta.env.VITE_OPENWORK_APP_VERSION": JSON.stringify(buildAppVersion), "import.meta.env.VITE_OPENWORK_BUILD_SHA": JSON.stringify(shortBuildSha), }, plugins: [ { name: "openwork-dev-server-id", configureServer(server) { server.middlewares.use("/__openwork_dev_server_id", (_req, res) => { res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify({ appRoot })); }); }, }, tailwindcss(), react({ babel: { plugins: [["babel-plugin-react-compiler", { compilationMode: "annotation" }]], }, }), ], server: { port: devPort, strictPort: true, ...(allowedHosts.size > 0 ? { allowedHosts: Array.from(allowedHosts) } : {}), ...(headlessDenTarget ? { proxy: { "/api/den": { target: headlessDenTarget, changeOrigin: true }, }, } : {}), }, build: { target: "esnext", rollupOptions: { input: { app: resolve(appRoot, "index.html"), overlay: resolve(appRoot, "overlay.html"), }, }, }, resolve: { alias: { "@": resolve(appRoot, "src"), }, }, });