#!/usr/bin/env node import { copyFile, lstat, mkdir, readdir, readFile, rm, symlink, writeFile, } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(__dirname, "..", ".."); const templatesDir = path.join(repoRoot, "backend", "bot", "app", "web", "templates"); const DEFAULT_OUT_DIR = path.join(repoRoot, "frontend-nginx-dist"); const outDir = resolveOutDir(); function resolveOutDir() { const argIndex = process.argv.indexOf("--out"); if (argIndex === -1) { return DEFAULT_OUT_DIR; } const rawValue = process.argv[argIndex + 1]; if (!rawValue) { throw new Error("--out requires a directory path"); } return path.resolve(process.cwd(), rawValue); } async function pathExists(filePath) { try { await lstat(filePath); return true; } catch (error) { if (error?.code === "ENOENT") return false; throw error; } } async function copyIfExists(sourceName, targetName = sourceName) { const sourcePath = path.join(templatesDir, sourceName); if (!(await pathExists(sourcePath))) return false; await copyFile(sourcePath, path.join(outDir, targetName)); return true; } async function linkOrCopy(targetName, aliasName) { const aliasPath = path.join(outDir, aliasName); try { await symlink(targetName, aliasPath); } catch (error) { if (!["EPERM", "EINVAL", "ENOSYS"].includes(error?.code)) { throw error; } await copyFile(path.join(outDir, targetName), aliasPath); } } function latestMatching(entries, pattern, fallbackName) { const matches = entries.filter((name) => pattern.test(name)).sort(); return matches.at(-1) || fallbackName; } async function copyRuntimeAsset({ hashedName, stableName }) { const copied = await copyIfExists(hashedName); if (copied && hashedName !== stableName) { await linkOrCopy(hashedName, stableName); } else if (!copied) { await copyIfExists(stableName); } const gzipName = `${hashedName}.gz`; const gzipCopied = await copyIfExists(gzipName); if (gzipCopied && hashedName !== stableName) { await linkOrCopy(gzipName, `${stableName}.gz`); } } function prepareIndexHtml(rawHtml, { cssName, jsName }) { const html = rawHtml .replace(/\r\n/g, "\n") .replace('href="/subscription_webapp.css"', `href="/${cssName}"`); const lines = html.split("\n"); const output = lines .map((line) => line.includes("WEBAPP_JS_SCRIPT") ? ` ` : line ) .filter( (line) => !line.includes("WEBAPP_I18N_SCRIPT") && !line.includes("WEBAPP_CONFIG_SCRIPT") && !line.includes("WEBAPP_DEV_MOCK_START") && !line.includes("WEBAPP_DEV_MOCK_END") && !line.includes('subscription_webapp.js" defer') ) .join("\n"); return output.endsWith("\n") ? output : `${output}\n`; } async function main() { const entries = await readdir(templatesDir); const mainJsName = latestMatching( entries, /^subscription_webapp\.min\.[0-9a-f]{8}\.js$/, "subscription_webapp.js" ); const mainCssName = latestMatching( entries, /^subscription_webapp\.[0-9a-f]{8}\.css$/, "subscription_webapp.css" ); const adminJsName = latestMatching( entries, /^subscription_webapp_admin\.min\.[0-9a-f]{8}\.js$/, "subscription_webapp_admin.js" ); const adminCssName = latestMatching( entries, /^subscription_webapp_admin\.[0-9a-f]{8}\.css$/, "subscription_webapp_admin.css" ); await rm(outDir, { recursive: true, force: true }); await mkdir(outDir, { recursive: true }); await Promise.all([ copyRuntimeAsset({ hashedName: mainJsName, stableName: "subscription_webapp.js" }), copyRuntimeAsset({ hashedName: mainCssName, stableName: "subscription_webapp.css" }), copyRuntimeAsset({ hashedName: adminJsName, stableName: "subscription_webapp_admin.js" }), copyRuntimeAsset({ hashedName: adminCssName, stableName: "subscription_webapp_admin.css" }), ]); const indexTemplate = await readFile(path.join(templatesDir, "subscription_webapp.html"), "utf8"); await writeFile( path.join(outDir, "index.html"), prepareIndexHtml(indexTemplate, { cssName: mainCssName, jsName: mainJsName }), "utf8" ); console.log( `Prepared nginx assets in ${path.relative(repoRoot, outDir)}: ${mainJsName}, ${mainCssName}, ${adminJsName}, ${adminCssName}` ); } await main();