diff --git a/.dockerignore b/.dockerignore index 434ec09..e76f0cf 100644 --- a/.dockerignore +++ b/.dockerignore @@ -17,6 +17,7 @@ docs-site/node_modules/ docs-site/.astro/ docs-site/dist/ docs-site/src/content/docs/ +frontend-nginx-dist/ deploy/compose/docker-compose-dev.yml data/* !data/tariffs.example.json @@ -51,6 +52,11 @@ bot/app/web/templates/subscription_webapp_admin.min.*.js.br bot/app/web/templates/subscription_webapp_admin.min.*.js.gz bot/app/web/templates/subscription_webapp_admin.*.css.br bot/app/web/templates/subscription_webapp_admin.*.css.gz +bot/app/web/templates/subscription_webapp_docs_demo.css +bot/app/web/templates/subscription_webapp_docs_demo.js +bot/app/web/templates/subscription_webapp_docs_demo.*.css +bot/app/web/templates/subscription_webapp_docs_demo.*.css.br +bot/app/web/templates/subscription_webapp_docs_demo.*.css.gz backend/bot/app/web/templates/subscription_webapp.css backend/bot/app/web/templates/subscription_webapp.js backend/bot/app/web/templates/subscription_webapp.min.*.js @@ -67,6 +73,11 @@ backend/bot/app/web/templates/subscription_webapp_admin.min.*.js.br backend/bot/app/web/templates/subscription_webapp_admin.min.*.js.gz backend/bot/app/web/templates/subscription_webapp_admin.*.css.br backend/bot/app/web/templates/subscription_webapp_admin.*.css.gz +backend/bot/app/web/templates/subscription_webapp_docs_demo.css +backend/bot/app/web/templates/subscription_webapp_docs_demo.js +backend/bot/app/web/templates/subscription_webapp_docs_demo.*.css +backend/bot/app/web/templates/subscription_webapp_docs_demo.*.css.br +backend/bot/app/web/templates/subscription_webapp_docs_demo.*.css.gz # Byte-compiled / optimized / DLL files **/__pycache__/ diff --git a/.gitignore b/.gitignore index ade87d4..8ff0f22 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ docs-site/.astro/ docs-site/dist/ docs-site/public/demo/runtime/ docs-site/src/content/docs/ +frontend-nginx-dist/ # WebApp build artifacts (regenerated by `npm run build:webapp` / Docker build) bot/app/web/templates/subscription_webapp.css diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile index 21d5085..66791a4 100644 --- a/deploy/docker/Dockerfile +++ b/deploy/docker/Dockerfile @@ -119,6 +119,8 @@ COPY frontend ./ RUN mkdir -p ../backend/bot/app/web/templates RUN npm run build:webapp +COPY backend/bot/app/web/templates/subscription_webapp.html ../backend/bot/app/web/templates/subscription_webapp.html +RUN node ./scripts/prepare_nginx_assets.mjs --out ../frontend-nginx-dist FROM nginx:1.27-alpine AS frontend @@ -131,57 +133,10 @@ LABEL org.opencontainers.image.source="https://github.com/3252a8/remnawave-minis COPY deploy/docker/frontend/nginx.conf /etc/nginx/conf.d/default.conf COPY deploy/docker/frontend/00-startup-banner.sh /docker-entrypoint.d/00-startup-banner.sh COPY deploy/docker/frontend/robots.txt /usr/share/nginx/html/robots.txt -COPY backend/bot/app/web/templates/subscription_webapp.html /usr/share/nginx/html/index.html -COPY --from=frontend-builder /app/backend/bot/app/web/templates/subscription_webapp.css /usr/share/nginx/html/subscription_webapp.css -COPY --from=frontend-builder /app/backend/bot/app/web/templates/subscription_webapp.*.css /usr/share/nginx/html/ -COPY --from=frontend-builder /app/backend/bot/app/web/templates/subscription_webapp.*.css.gz /usr/share/nginx/html/ -COPY --from=frontend-builder /app/backend/bot/app/web/templates/subscription_webapp_admin.css /usr/share/nginx/html/subscription_webapp_admin.css -COPY --from=frontend-builder /app/backend/bot/app/web/templates/subscription_webapp_admin.*.css /usr/share/nginx/html/ -COPY --from=frontend-builder /app/backend/bot/app/web/templates/subscription_webapp_admin.*.css.gz /usr/share/nginx/html/ -COPY --from=frontend-builder /app/backend/bot/app/web/templates/subscription_webapp.js /usr/share/nginx/html/subscription_webapp.js -COPY --from=frontend-builder /app/backend/bot/app/web/templates/subscription_webapp.min.*.js /usr/share/nginx/html/ -COPY --from=frontend-builder /app/backend/bot/app/web/templates/subscription_webapp.min.*.js.gz /usr/share/nginx/html/ -COPY --from=frontend-builder /app/backend/bot/app/web/templates/subscription_webapp_admin.js /usr/share/nginx/html/subscription_webapp_admin.js -COPY --from=frontend-builder /app/backend/bot/app/web/templates/subscription_webapp_admin.min.*.js /usr/share/nginx/html/ -COPY --from=frontend-builder /app/backend/bot/app/web/templates/subscription_webapp_admin.min.*.js.gz /usr/share/nginx/html/ - RUN set -eu; \ find /docker-entrypoint.d -type f -name '*.sh' -exec sed -i 's/\r$//' {} +; \ - HASHED=$(ls /usr/share/nginx/html/subscription_webapp.min.*.js 2>/dev/null | sort | tail -n1 | xargs -n1 basename || true); \ - JS_NAME="${HASHED:-subscription_webapp.js}"; \ - CSS_NAME="subscription_webapp.css"; \ - ADMIN_HASHED=$(ls /usr/share/nginx/html/subscription_webapp_admin.min.*.js 2>/dev/null | sort | tail -n1 | xargs -n1 basename || true); \ - ADMIN_CSS_NAME="subscription_webapp_admin.css"; \ - for candidate in /usr/share/nginx/html/subscription_webapp.*.css; do \ - name="$(basename "$candidate")"; \ - case "$name" in \ - subscription_webapp.[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f].css) CSS_NAME="$name" ;; \ - esac; \ - done; \ - for candidate in /usr/share/nginx/html/subscription_webapp_admin.*.css; do \ - name="$(basename "$candidate")"; \ - case "$name" in \ - subscription_webapp_admin.[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f].css) ADMIN_CSS_NAME="$name" ;; \ - esac; \ - done; \ - if [ -n "$ADMIN_HASHED" ]; then \ - cp "/usr/share/nginx/html/${ADMIN_HASHED}" /usr/share/nginx/html/subscription_webapp_admin.js; \ - if [ -f "/usr/share/nginx/html/${ADMIN_HASHED}.gz" ]; then cp "/usr/share/nginx/html/${ADMIN_HASHED}.gz" /usr/share/nginx/html/subscription_webapp_admin.js.gz; fi; \ - fi; \ - if [ "$ADMIN_CSS_NAME" != "subscription_webapp_admin.css" ]; then \ - cp "/usr/share/nginx/html/${ADMIN_CSS_NAME}" /usr/share/nginx/html/subscription_webapp_admin.css; \ - if [ -f "/usr/share/nginx/html/${ADMIN_CSS_NAME}.gz" ]; then cp "/usr/share/nginx/html/${ADMIN_CSS_NAME}.gz" /usr/share/nginx/html/subscription_webapp_admin.css.gz; fi; \ - fi; \ - sed -i \ - -e '/WEBAPP_I18N_SCRIPT/d' \ - -e '/WEBAPP_CONFIG_SCRIPT/d' \ - -e "s|href=\"/subscription_webapp.css\"|href=\"/${CSS_NAME}\"|" \ - -e "/WEBAPP_JS_SCRIPT/c\\ " \ - -e '/WEBAPP_DEV_MOCK_START/d' \ - -e '/WEBAPP_DEV_MOCK_END/d' \ - -e '/subscription_webapp.js" defer/d' \ - /usr/share/nginx/html/index.html; \ chmod +x /docker-entrypoint.d/00-startup-banner.sh +COPY --from=frontend-builder /app/frontend-nginx-dist/ /usr/share/nginx/html/ COPY --from=version-builder /build-tag /build-tag COPY --from=version-builder /build-commit /build-commit diff --git a/frontend/scripts/prepare_nginx_assets.mjs b/frontend/scripts/prepare_nginx_assets.mjs new file mode 100644 index 0000000..01714b3 --- /dev/null +++ b/frontend/scripts/prepare_nginx_assets.mjs @@ -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") + ? ` ` + : 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();