Serve the web client from the daemon (#1635)

* Serve the web client from the daemon

Keep the bundled browser UI opt-in and exclude it from desktop packaging so desktop builds do not ship a duplicate renderer.

* Escape daemon web UI bootstrap hint

* Fix bundled web UI dist path
This commit is contained in:
Mohamed Boudra
2026-06-26 12:00:11 +08:00
committed by GitHub
parent 3d6b4adc68
commit 4e2c06ec71
24 changed files with 1302 additions and 6 deletions

View File

@@ -0,0 +1,141 @@
import { spawn } from "node:child_process";
import { createReadStream, createWriteStream } from "node:fs";
import { cp, mkdir, readdir, rm, stat } from "node:fs/promises";
import path from "node:path";
import { pipeline } from "node:stream/promises";
import { fileURLToPath } from "node:url";
import { constants as zlibConstants, createBrotliCompress, createGzip } from "node:zlib";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, "..");
const APP_DIR = path.join(REPO_ROOT, "packages", "app");
const SOURCE_DIST = path.join(APP_DIR, "dist");
const TARGET_DIST = path.join(REPO_ROOT, "packages", "server", "dist", "server", "web-ui");
const COMPRESS_EXTENSIONS = new Set([".html", ".js", ".css", ".json", ".svg", ".map"]);
function fmtMiB(bytes) {
return `${(bytes / 1024 / 1024).toFixed(2)} MiB`;
}
function run(command, args, options) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
stdio: "inherit",
shell: false,
...options,
});
child.on("error", reject);
child.on("close", (code) => {
if (code !== 0) {
reject(new Error(`Command failed with exit code ${code}: ${command} ${args.join(" ")}`));
return;
}
resolve();
});
});
}
async function exportBrowserWebApp() {
console.log("Exporting browser web app...");
await run("npm", ["run", "build:web", "--workspace=@getpaseo/app"], {
cwd: REPO_ROOT,
});
}
async function cleanTarget() {
console.log(`Cleaning ${path.relative(REPO_ROOT, TARGET_DIST)}...`);
await rm(TARGET_DIST, { recursive: true, force: true });
await mkdir(TARGET_DIST, { recursive: true });
}
async function copyAssets() {
console.log(`Copying assets to ${path.relative(REPO_ROOT, TARGET_DIST)}...`);
await cp(SOURCE_DIST, TARGET_DIST, { recursive: true, force: true });
}
async function compressFile(filePath) {
const brotliPath = `${filePath}.br`;
const gzipPath = `${filePath}.gz`;
await Promise.all([
pipeline(
createReadStream(filePath),
createBrotliCompress({
params: {
[zlibConstants.BROTLI_PARAM_QUALITY]: zlibConstants.BROTLI_MAX_QUALITY,
},
}),
createWriteStream(brotliPath),
),
pipeline(createReadStream(filePath), createGzip(), createWriteStream(gzipPath)),
]);
}
async function precompressAssets(dir) {
const entries = await readdir(dir, { withFileTypes: true });
const files = entries.filter((entry) => entry.isFile());
const dirs = entries.filter((entry) => entry.isDirectory());
for (const file of files) {
const filePath = path.join(dir, file.name);
if (COMPRESS_EXTENSIONS.has(path.extname(file.name).toLowerCase())) {
await compressFile(filePath);
}
}
for (const subdir of dirs) {
await precompressAssets(path.join(dir, subdir.name));
}
}
async function measureBundle(dir) {
let raw = 0;
let gzip = 0;
let brotli = 0;
async function walk(current) {
const entries = await readdir(current, { withFileTypes: true });
for (const entry of entries) {
const entryPath = path.join(current, entry.name);
if (entry.isDirectory()) {
await walk(entryPath);
continue;
}
const info = await stat(entryPath);
const ext = path.extname(entry.name).toLowerCase();
if (ext === ".br") {
brotli += info.size;
} else if (ext === ".gz") {
gzip += info.size;
} else {
raw += info.size;
}
}
}
await walk(dir);
return { raw, gzip, brotli };
}
async function main() {
await exportBrowserWebApp();
const sourceStat = await stat(SOURCE_DIST).catch(() => null);
if (!sourceStat?.isDirectory()) {
throw new Error(`Browser web export not found at ${SOURCE_DIST}`);
}
await cleanTarget();
await copyAssets();
await precompressAssets(TARGET_DIST);
const sizes = await measureBundle(TARGET_DIST);
console.log("Daemon web UI bundle:");
console.log(` raw: ${fmtMiB(sizes.raw)}`);
console.log(` gzip: ${fmtMiB(sizes.gzip)}`);
console.log(` brotli: ${fmtMiB(sizes.brotli)}`);
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});