Files
openwork/apps/app/vite.config.ts
ben 47db4e39e3 feat(desktop): migration engine for Tauri → Electron (draft, do not merge) (#1523)
* feat(desktop): migration engine for Tauri → Electron handoff

Implements the infrastructure needed to move users from Tauri to Electron
without losing state or Launchpad/Dock presence. Zero user impact on its
own — this becomes live only when paired with a final Tauri release that
calls the snapshot writer and installs Electron.

What lands here:

- Unify app identity with Tauri. electron-builder appId goes from
  com.differentai.openwork.electron → com.differentai.openwork so macOS
  reuses the same bundle identifier (no duplicate Dock icon, no new
  Gatekeeper prompt, same TCC permissions).
- Unify userData path. Electron's app.setPath("userData", ...) now points
  at the exact folder Tauri uses (~/Library/Application Support/com.differentai.openwork
  on macOS, %APPDATA%/com.differentai.openwork on Windows, ~/.config/
  com.differentai.openwork on Linux). An OPENWORK_ELECTRON_USERDATA env
  var override is available for dogfooders who want isolation.
- Filename compat. On first launch, Electron copies Tauri's
  openwork-workspaces.json → workspace-state.json (leaves the legacy
  file in place for rollback safety).
- electron-updater wiring. New deps + IPC handlers
  (openwork:updater:check / download / installAndRestart). Packaged-only.
  Publish config points at the same GitHub release as Tauri.
- Migration snapshot plumbing.
  * Tauri Rust command `write_migration_snapshot` serializes an
    allowlist of localStorage keys to app_data_dir/migration-snapshot.v1.json.
  * `apps/app/src/app/lib/migration.ts` has matching
    writeMigrationSnapshotFromTauri() / ingestMigrationSnapshotOnElectronBoot().
  * Scope: workspace list + selection only
    (openwork.react.activeWorkspace, .sessionByWorkspace,
     openwork.server.list/active/urlOverride/token). Everything else is
    cheap to redo.
  * Electron main exposes openwork:migration:read / ack IPC; preload
    bridges them under window.__OPENWORK_ELECTRON__.migration.
  * desktop-runtime-boot.ts ingests the snapshot once on first launch,
    hydrates empty localStorage keys, then acks.
- Updated prds/electron-migration-plan.md with the localStorage scope
  decision and the remaining work (last Tauri release ships the UI
  prompt + installer downloader).

Verified: `pnpm --filter @openwork/app typecheck` ✓,
`pnpm --filter @openwork/desktop build:electron` ✓, `cargo check`
against src-tauri ✓.

* fix(desktop): emit relative asset paths for Electron packaged builds

Packaged Electron loads index.html via file:// (Contents/Resources/app-dist/
index.html inside the .app bundle), but Vite was building with the default
base: '/' which resolves /assets/*.js to the filesystem root. Result: a
working dev experience (Vite dev server on localhost:5173) and a broken
packaged .app that renders an empty <div id="root"></div>.

The Tauri shell doesn't hit this because Tauri serves the built HTML
through its own tauri:// protocol which rewrites paths. Electron's
file:// loader has no such rewriter.

Fix: electron-build.mjs sets OPENWORK_ELECTRON_BUILD=1 when invoking
vite build, and vite.config.ts flips base to './' only when that env
var is set. Dev server and Tauri builds unchanged.

Discovered while manually exercising the Electron prod-build migration
flow (packaged app launch, snapshot ingest, idempotency, updater IPC).
Latent since PR #1522 landed the packaged build path; never caught
because dogfood ran via dev:electron which uses the Vite dev server.

---------

Co-authored-by: Benjamin Shafii <benjamin@openworklabs.com>
2026-04-22 16:50:51 -07:00

58 lines
1.8 KiB
TypeScript

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)));
// 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";
export default defineConfig({
base: isElectronPackagedBuild ? "./" : "/",
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(),
],
server: {
port: devPort,
strictPort: true,
...(allowedHosts.size > 0 ? { allowedHosts: Array.from(allowedHosts) } : {}),
},
build: {
target: "esnext",
},
});