mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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:
141
scripts/build-daemon-web-ui.mjs
Normal file
141
scripts/build-daemon-web-ui.mjs
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user