Files
openwork/apps/app/vite.config.ts
ben 7a53c2964c feat(desktop): Tauri → Electron migration release kit (#1526)
Ships the last-missing piece of the Electron migration. After this PR
merges there are three one-shot scripts under scripts/migration/ that
carry the whole lifecycle from "cut v0.12.0" through "delete src-tauri":

  01-cut-migration-release.mjs   # bumps versions, writes the release env
                                 # fragment, tags, pushes.
  02-validate-migration.mjs      # guided smoke test against a cut release.
  03-post-migration-cleanup.mjs  # deletes src-tauri, flips defaults,
                                 # scrubs Tauri docs (dry-run by default).

Code landing in the same PR (dormant until a release sets
VITE_OPENWORK_MIGRATION_RELEASE=1):

- apps/desktop/src-tauri/src/commands/migration.rs gains
  migrate_to_electron() — downloads the matching Electron .zip, verifies
  the Apple signature, swaps the .app bundle in place via a detached
  shell script, relaunches, and exits. Windows + Linux branches are
  stubbed with clear TODOs for the follow-up.
- apps/app/src/app/lib/migration.ts grows migrateToElectron() + a
  "later" defer helper.
- apps/app/src/react-app/shell/migration-prompt.tsx adds the one-time
  "OpenWork is moving to a new engine" modal. Mounted from
  providers.tsx. Gated on isTauriRuntime() AND the build-time flag, so
  Electron users and all non-migration-release builds never render it.
- apps/app/vite.config.ts loads apps/app/.env.migration-release when
  present so the prompt gets the release-specific download URLs.
- .gitignore allows the migration-release fragment to be committed only
  on the tagged migration-release commit (removed in cleanup step).

Release workflow:
- .github/workflows/release-macos-aarch64.yml gains a publish-electron
  job alongside the Tauri jobs. Gated on RELEASE_PUBLISH_ELECTRON repo
  var OR the new publish_electron workflow_dispatch input (default
  false). Uses the existing Apple Dev ID secrets — no new credential
  story. Produces latest-mac.yml alongside Tauri's latest.json so a
  v0.12.0 release serves both updaters.

Verified:
  pnpm --filter @openwork/app typecheck   ✓
  cargo check --manifest-path apps/desktop/src-tauri/Cargo.toml   ✓
  node --check on all mjs scripts   ✓
  python yaml parse of release-macos-aarch64.yml   ✓

Not verified (needs a real release cycle):
  end-to-end migration from a signed Tauri .app to a signed Electron
  .app through the detached-script install swap.

Co-authored-by: Benjamin Shafii <benjamin@openworklabs.com>
2026-04-22 18:52:30 -07:00

88 lines
3.0 KiB
TypeScript

import os from "node:os";
import { existsSync, readFileSync } from "node:fs";
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)));
// 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.indexOf("=");
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";
export default defineConfig({
base: isElectronPackagedBuild ? "./" : "/",
define: Object.fromEntries(
Object.entries(migrationReleaseEnv).map(([k, v]) => [
`import.meta.env.${k}`,
JSON.stringify(v),
]),
),
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",
},
});