Files

166 lines
5.4 KiB
TypeScript

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<string>();
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 | undefined>): 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<string, string> {
const fragmentPath = resolve(appRoot, ".env.migration-release");
if (!existsSync(fragmentPath)) return {};
const out: Record<string, string> = {};
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"),
},
},
});