+
+
+
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);
+}