mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
* 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
142 lines
4.2 KiB
JavaScript
142 lines
4.2 KiB
JavaScript
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);
|
|
});
|