Add scripts for running Yaak in a browser (#664)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Gregory Schier
2026-09-15 10:12:53 -07:00
committed by GitHub
co-authored by Claude Opus 5
parent 881cff472d
commit 48889b6550
5 changed files with 141 additions and 17 deletions
+3
View File
@@ -3,6 +3,9 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" type="image/png" href="/icon-128.png" sizes="128x128" />
<link rel="apple-touch-icon" href="/icon-128.png" />
<title>Yaak App</title> <title>Yaak App</title>
<!-- <script src="http://localhost:8097"></script>--> <!-- <script src="http://localhost:8097"></script>-->
+10
View File
@@ -16,6 +16,12 @@ const standardFontsDir = normalizePath(
path.join(path.dirname(require.resolve("pdfjs-dist/package.json")), "standard_fonts"), 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 * 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 * browser tab, with its own IndexedDB store instead of the Rust engine; anything
@@ -62,6 +68,10 @@ export default defineConfig(async () => {
targets: [ targets: [
{ src: cMapsDir, dest: "" }, { src: cMapsDir, dest: "" },
{ src: standardFontsDir, 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" },
], ],
}), }),
], ],
+7 -17
View File
@@ -56,24 +56,14 @@ execSync("wasm-pack build --target bundler", {
env: { env: {
...process.env, ...process.env,
CC_wasm32_unknown_unknown: clang, 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`, RUSTFLAGS: `--remap-path-prefix=${cargoHome}=/cargo --remap-path-prefix=${sysroot}=/rustc`,
}, },
}); });
// Rewrite the generated entry to use Vite's ?init import style instead of // No post-processing: wasm-pack's own entry (pkg/yaak_wasm.js) is what the app
// the ES Module Integration style that wasm-pack generates, which Vite/rolldown // imports, and `vite-plugin-wasm` handles its ES Module Integration form in both
// does not support in production builds. // dev and production builds. A rewrite used to live here for a Vite that could
const entry = path.join(__dirname, "pkg", "yaak_web.js"); // not, and it outlived both that Vite and the crate name it hardcoded.
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"),
);
+5
View File
@@ -83,6 +83,11 @@
"client:bundle": "node scripts/run-build.mjs client --config crates-tauri/yaak-app-client/tauri.release.conf.json --no-sign", "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:build": "node scripts/run-build.mjs proxy",
"proxy:dev": "node scripts/run-dev.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", "migration": "node scripts/create-migration.cjs",
"build": "npm run --workspaces --if-present build", "build": "npm run --workspaces --if-present build",
"test": "npm run --workspaces --if-present test", "test": "npm run --workspaces --if-present test",
+116
View File
@@ -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);
}