From 48889b6550cdc4814aa8e5619be19fbefb66bd7f Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 15 Sep 2026 10:12:53 -0700 Subject: [PATCH] Add scripts for running Yaak in a browser (#664) Co-authored-by: Claude Opus 5 --- apps/yaak-client/index.html | 3 + apps/yaak-client/vite.config.ts | 10 +++ crates/yaak-wasm/build-wasm.cjs | 24 ++----- package.json | 5 ++ scripts/run-web.mjs | 116 ++++++++++++++++++++++++++++++++ 5 files changed, 141 insertions(+), 17 deletions(-) create mode 100644 scripts/run-web.mjs diff --git a/apps/yaak-client/index.html b/apps/yaak-client/index.html index 9417af14..deaeb435 100644 --- a/apps/yaak-client/index.html +++ b/apps/yaak-client/index.html @@ -3,6 +3,9 @@ + + + Yaak App diff --git a/apps/yaak-client/vite.config.ts b/apps/yaak-client/vite.config.ts index 6fb1e7b8..27694a03 100644 --- a/apps/yaak-client/vite.config.ts +++ b/apps/yaak-client/vite.config.ts @@ -16,6 +16,12 @@ const standardFontsDir = normalizePath( path.join(path.dirname(require.resolve("pdfjs-dist/package.json")), "standard_fonts"), ); +// The app's own icons, served as the page icon rather than copied into the client: +// the bundled app and the tab should not be able to disagree about what Yaak looks like. +const iconsDir = normalizePath( + path.join(import.meta.dirname, "../../crates-tauri/yaak-app-client/icons/release"), +); + /** * Which host the platform package installs. `web` builds Yaak to run in a plain * browser tab, with its own IndexedDB store instead of the Rust engine; anything @@ -62,6 +68,10 @@ export default defineConfig(async () => { targets: [ { src: cMapsDir, dest: "" }, { src: standardFontsDir, dest: "" }, + // `/favicon.ico` is requested by browsers whether or not anything links to it, + // so it is served under that name to keep a 404 out of every console. + { src: `${iconsDir}/icon.ico`, dest: "", rename: "favicon.ico" }, + { src: `${iconsDir}/128x128.png`, dest: "", rename: "icon-128.png" }, ], }), ], diff --git a/crates/yaak-wasm/build-wasm.cjs b/crates/yaak-wasm/build-wasm.cjs index cbe5fc6a..220cb029 100644 --- a/crates/yaak-wasm/build-wasm.cjs +++ b/crates/yaak-wasm/build-wasm.cjs @@ -56,24 +56,14 @@ execSync("wasm-pack build --target bundler", { env: { ...process.env, CC_wasm32_unknown_unknown: clang, - AR_wasm32_unknown_unknown: fs.existsSync(ar) ? ar : (process.env.AR_wasm32_unknown_unknown ?? ""), + AR_wasm32_unknown_unknown: fs.existsSync(ar) + ? ar + : (process.env.AR_wasm32_unknown_unknown ?? ""), RUSTFLAGS: `--remap-path-prefix=${cargoHome}=/cargo --remap-path-prefix=${sysroot}=/rustc`, }, }); -// Rewrite the generated entry to use Vite's ?init import style instead of -// the ES Module Integration style that wasm-pack generates, which Vite/rolldown -// does not support in production builds. -const entry = path.join(__dirname, "pkg", "yaak_web.js"); -fs.writeFileSync( - entry, - [ - 'import init from "./yaak_web_bg.wasm?init";', - 'export * from "./yaak_web_bg.js";', - 'import * as bg from "./yaak_web_bg.js";', - 'const instance = await init({ "./yaak_web_bg.js": bg });', - "bg.__wbg_set_wasm(instance.exports);", - "instance.exports.__wbindgen_start();", - "", - ].join("\n"), -); +// No post-processing: wasm-pack's own entry (pkg/yaak_wasm.js) is what the app +// imports, and `vite-plugin-wasm` handles its ES Module Integration form in both +// dev and production builds. A rewrite used to live here for a Vite that could +// not, and it outlived both that Vite and the crate name it hardcoded. diff --git a/package.json b/package.json index c6c38092..e05eafd1 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,11 @@ "client:bundle": "node scripts/run-build.mjs client --config crates-tauri/yaak-app-client/tauri.release.conf.json --no-sign", "proxy:build": "node scripts/run-build.mjs proxy", "proxy:dev": "node scripts/run-dev.mjs proxy", + "web:dev": "run-p web:dev:*", + "web:dev:app": "node scripts/run-web.mjs dev", + "web:dev:proxy": "node scripts/run-web.mjs proxy", + "web:build": "node scripts/run-web.mjs build", + "web:serve": "node scripts/run-web.mjs serve", "migration": "node scripts/create-migration.cjs", "build": "npm run --workspaces --if-present build", "test": "npm run --workspaces --if-present test", diff --git a/scripts/run-web.mjs b/scripts/run-web.mjs new file mode 100644 index 00000000..ceea0b69 --- /dev/null +++ b/scripts/run-web.mjs @@ -0,0 +1,116 @@ +#!/usr/bin/env node + +/** + * Run Yaak in a browser: the client built for the web target, and the server + * that executes its sends. + * + * `YAAK_TARGET=web` is what resolves `@yaakapp-internal/platform` to its browser + * entry (see apps/yaak-client/vite.config.ts). Setting it in a node script rather + * than inline in an npm script keeps this working on Windows. + */ + +import { spawn, spawnSync } from "child_process"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const rootDir = path.join(__dirname, ".."); + +// The post-checkout hook writes a per-worktree dev port in here, and the client's +// vite config reads YAAK_CLIENT_DEV_PORT from it. Without this, two worktrees both +// try to serve on 1420. (run-dev.mjs reads the same file, but with Object.assign, +// so there an explicit YAAK_CLIENT_DEV_PORT is silently ignored.) +const envLocalPath = path.join(rootDir, ".env.local"); +if (fs.existsSync(envLocalPath)) { + for (const line of fs.readFileSync(envLocalPath, "utf8").split("\n")) { + if (!line || line.startsWith("#")) continue; + const [key, value] = line.split("="); + if (!key || !value) continue; + // Defaults, not overrides: a variable set on the command line is the more + // deliberate of the two and has to win, or `PORT=x vp run web:dev` silently + // lands somewhere else. + process.env[key.trim()] ??= value.trim(); + } +} + +const DIST = "dist/apps/yaak-client"; + +const [mode] = process.argv.slice(2); + +// Invoke the Vite+ CLI JS entry point directly via node, the way run-dev.mjs invokes +// the Tauri CLI. The `.bin/vp` shim is a POSIX script that npm pairs with `vp.cmd` and +// `vp.ps1` on Windows, so spawning it without a shell only works on one platform. +const vp = path.join(rootDir, "node_modules", "vite-plus", "bin", "vp"); + +const runVite = (args) => [process.execPath, [vp, ...args]]; + +const env = (extra = {}) => ({ ...process.env, YAAK_TARGET: "web", ...extra }); + +/** Run a build step to completion. A failure ends the script. */ +function run(command, args, extra = {}) { + const result = spawnSync(command, args, { cwd: rootDir, stdio: "inherit", env: env(extra) }); + if (result.status !== 0) process.exit(result.status ?? 1); +} + +/** + * Run a server until it stops, and take it down when this process is stopped. + * + * Servers are started as a direct child rather than through `cargo run`, and + * signals are forwarded to them, because neither happens by itself: `cargo run` + * is a wrapper that does not pass a terminating signal on to the binary it + * spawned, and a child is not killed by its parent exiting. Either one on its + * own leaves a server holding the port after the terminal that started it has + * gone, and the next run fails with "address already in use". + */ +function serve(command, args, extra = {}) { + const child = spawn(command, args, { cwd: rootDir, stdio: "inherit", env: env(extra) }); + + const stop = (signal) => child.killed || child.kill(signal); + for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(signal, () => stop(signal)); + process.on("exit", () => stop("SIGTERM")); + + child.on("exit", (code, signal) => process.exit(code ?? (signal ? 1 : 0))); + child.on("error", (err) => { + console.error(`Failed to start ${command}: ${err.message}`); + process.exit(1); + }); +} + +/** Build the server and hand back the binary, so it can be run without a wrapper. */ +function buildServer() { + run("cargo", ["build", "-p", "yaak-web"]); + const target = process.env.CARGO_TARGET_DIR ?? path.join(rootDir, "target"); + return path.join(target, "debug", "yaak-web"); +} + +switch (mode) { + // The app, with hot reload, on one origin: its own `/v1` is passed through to + // the server `web:dev` starts alongside it. Nothing to build first and one + // address to open. + case "dev": + serve(...runVite(["-C", "apps/yaak-client", "dev", "--force"])); + break; + + // The send executor behind the dev server, on the port a dev build looks for. + case "proxy": + serve(buildServer(), []); + break; + + case "build": + run(...runVite(["-C", "apps/yaak-client", "build"])); + break; + + // One process serving both, the shape the Docker image runs. The build comes + // first because serving a stale `dist` silently tests the last change but one. + case "serve": { + const server = buildServer(); + run(...runVite(["-C", "apps/yaak-client", "build"])); + serve(server, ["--serve", DIST]); + break; + } + + default: + console.error(`Unknown mode ${JSON.stringify(mode)}; expected dev, proxy, build or serve`); + process.exit(1); +}