build: reduce docker image layer churn
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
#!/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")
|
||||
? ` <script src="/${jsName}" type="module"></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();
|
||||
Reference in New Issue
Block a user