/* eslint-env node */
/**
* 多品牌静态首页生成器(构建后处理)
* ============================================================
* 目的:
* 一套 React 产物(build/)服务于 sites_config 中的 N 个品牌站点。
* 首页 index.html 里的 SEO/分享元信息(
、description、og:* 等)
* 原本是 %%占位符%%,靠 load.m.js 在浏览器里运行时按 hostname 改写。
* 但爬虫 / 分享卡片抓取时往往不执行 JS,拿到的还是占位符。
*
* 本脚本在 `craco build` + hash-loadm 之后运行:
* 1) 从 public/js/load.m.js 提取 sites_config / regions / HOST_ALIAS(单一数据源,不复制配置)。
* 2) 以 build/index.html(已含带 hash 的资源引用)为模板,
* 逐站点把 %%占位符%% 替换成该品牌的真实内容,写出 build//index.html。
* 3) 顺带生成配套的 CloudFront Function(host → folder,运行时 cloudfront-js-2.0),
* 写到项目根 cloudfront-host-to-folder.js,逻辑与 load.m.js 的 getSiteConfig()
* 路由保持一致,保证「静态首页」与「运行时」选到同一个品牌。
*
* folder(文件夹名)= sites_config 的 key = 域名主名(second-level label)。
* 例:mintpayouts.com → folder "mintpayouts";usslot777.com → 走 HOST_ALIAS → "default"。
*
* 用法:node scripts/generate-sites.js (已挂到 package.json 的 build 脚本)
*/
const fs = require("fs");
const path = require("path");
const vm = require("vm");
const ROOT = path.resolve(__dirname, "..");
// 配置单一数据源:public/js/load.m.js(不在别处复制一份)。
const LOADM = path.join(ROOT, "public", "js", "load.m.js");
// 模板与输出都在 build/:它已被 craco/hash 处理过(带 hash 的 load.m.js / main.js / css)。
const TEMPLATE = path.join(ROOT, "build", "index.html");
const OUT_DIR = path.join(ROOT, "build");
// CloudFront Function 生成路径(在项目根,不在 build/ 内,避免被下次 build 清空)。
const CF_OUT = path.join(ROOT, "cloudfront-host-to-folder.js");
// 与 load.m.js applyPageMeta() 保持一致的标题后缀(运行时也用这个,避免标题闪烁)。
const TITLE_SUFFIX = "Online Slots & Real Money Games";
/* ───────────────────────── 1. 提取 load.m.js 中的配置 ───────────────────────── */
/**
* sites_config / regions / themes / res_path 都是 IIFE 内的私有 const,无法 require。
* 但从 `const res_path` 到 `const getPathPrefix`(第一个用到 window/document 的函数)之间,
* 全是纯数据声明(res_path、各主题对象、regions、DEFAULT_REGION、sites_config),
* 不依赖任何浏览器 API。整段在禁用动态代码生成、无 Node 全局对象的隔离上下文中求值,
* 避免构建时把配置文件当作拥有 Node 权限的任意代码执行。
*/
function extractConfig(src) {
const start = src.indexOf("const res_path");
const end = src.indexOf("const getPathPrefix");
if (start === -1 || end === -1 || end <= start) {
throw new Error("[generate-sites] 无法在 load.m.js 中定位配置数据块(res_path … getPathPrefix)");
}
const block = src.slice(start, end);
const result = evaluateData(
block + "\n;({ res_path, sites_config, regions, DEFAULT_REGION });",
"config"
);
if (!result || typeof result.sites_config !== "object" || typeof result.regions !== "object") {
throw new Error("[generate-sites] 配置数据结构无效");
}
return result;
}
function evaluateData(source, label) {
const context = vm.createContext(Object.create(null), {
name: `generate-sites-${label}`,
codeGeneration: { strings: false, wasm: false },
});
const result = vm.runInContext(`"use strict";\n${source}`, context, { timeout: 1000 });
// Strip cross-realm prototypes and retain JSON-compatible configuration data only.
return JSON.parse(JSON.stringify(result));
}
/**
* HOST_ALIAS 在 getSiteConfig() 内部,单独用正则切出对象字面量再求值。
* 该字面量没有嵌套大括号,第一个 `}` 即结束。允许内部含注释/尾逗号。
*/
function extractHostAlias(src) {
const m = src.match(/const\s+HOST_ALIAS\s*=\s*(\{[\s\S]*?\})\s*;/);
if (!m) return {};
try {
const result = evaluateData(`(${m[1]});`, "host-alias");
return result && typeof result === "object" ? result : {};
} catch (e) {
console.warn("[generate-sites] 解析 HOST_ALIAS 失败,按空表处理:" + e.message);
return {};
}
}
/* ───────────────────────── 2. 占位符替换 ───────────────────────── */
const escAttr = (s) =>
String(s == null ? "" : s)
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
function buildReplacements(key, conf, regions, DEFAULT_REGION, domainFor) {
const region = regions[conf.region || DEFAULT_REGION] || regions[DEFAULT_REGION] || {};
const suffix = region.titleSuffix || TITLE_SUFFIX; // 运行时 applyPageMeta 用固定后缀;如需按区域可改 region.titleSuffix
const fullTitle = `${conf.title} | ${suffix}`;
const domain = domainFor(key);
const origin = `https://${domain}`;
const ogImage = conf.icon ? origin + conf.icon : "";
// theme-color:用各主题共有的深色背景(与首屏 loading 背景一致),运行时 setThemeColor 也会接管。
const themeColor = (conf.theme && (conf.theme.bodyBgColor || conf.theme.mainBgColor)) || "#0d1016";
return {
"%%BRAND_TITLE%%": escAttr(fullTitle),
"%%BRAND_DESCRIPTION%%": escAttr(conf.subtitle),
"%%THEME_COLOR%%": escAttr(themeColor),
"%%BRAND_NAME%%": escAttr(conf.title),
"%%BRAND_OG_TITLE%%": escAttr(fullTitle),
"%%BRAND_OG_DESCRIPTION%%": escAttr(conf.subtitle),
"%%BRAND_OG_IMAGE%%": escAttr(ogImage),
"%%BRAND_URL%%": escAttr(origin + "/"),
"%%FAV_ICO%%": escAttr(conf.favicon || "/resources/favicons/favicon.ico"),
};
}
function applyReplacements(template, map) {
let html = template;
for (const token in map) {
html = html.split(token).join(map[token]);
}
return html;
}
/* ───────────────────────── 3. CloudFront Function 生成 ───────────────────────── */
/**
* 生成 host → folder 的 CloudFront Function(viewer-request,运行时 cloudfront-js-2.0)。
* 逻辑对齐 load.m.js getSiteConfig():匹配完整域名或任意子域名下的 HOST_ALIAS,
* 再从主域名方向查找品牌标签 → 校验 folder 是否存在 → 否则 default。
* 仅改写「文档请求」("/"、扩展名为空的 SPA 路由、index.html);静态资源(带扩展名)保持原路径,从根目录回源。
*/
function generateCloudFrontFn(folders, hostAlias) {
const foldersMap = {};
folders.forEach((f) => (foldersMap[f] = true));
const foldersJson = JSON.stringify(foldersMap, null, 2);
const aliasJson = JSON.stringify(hostAlias, null, 2);
return `/* eslint-disable */
/**
* CloudFront Function (viewer-request) —— host → 品牌文件夹路由
* ============================================================
* 由 scripts/generate-sites.js 自动生成,请勿手改(改 sites_config 后重跑 npm run build)。
*
* 作用:按访问域名把「文档请求」改写到 //index.html,命中对应品牌的静态首页。
* 静态资源(带扩展名,如 .js/.css/.webp/.ico)原样透传,从桶根目录回源。
* 回源到 S3 前先把 URI 里连续的 // 合并为 /(等价 nginx merge_slashes on),
* 避免 //a//b.js 这类多斜杠路径在 S3 里 key 不匹配而 404(S3 不做斜杠归一)。
* 运行时:cloudfront-js-2.0。
* 关联:与 public/js/load.m.js 的 getSiteConfig() 路由保持一致。
*/
var FOLDERS = ${foldersJson};
var HOST_ALIAS = ${aliasJson};
function pickFolder(rawHost) {
var host = (rawHost || "").toLowerCase();
var colon = host.indexOf(":");
if (colon !== -1) host = host.slice(0, colon);
host = host.replace(/\\.$/, "");
// 别名同时匹配裸域名和任意层级子域名,例如 m.ukslot777.com。
for (var aliasHost in HOST_ALIAS) {
if (host === aliasHost || host.endsWith("." + aliasHost)) {
return FOLDERS[HOST_ALIAS[aliasHost]] === true ? HOST_ALIAS[aliasHost] : "default";
}
}
// 从主域名方向查找品牌标签,例如 app.m.blazingreels777.com → blazingreels777。
var parts = host.split(".");
for (var i = parts.length - 1; i >= 0; i--) {
if (FOLDERS[parts[i]] === true) return parts[i];
}
return "default";
}
// nginx merge_slashes:把路径里 2 个及以上连续的 / 压成 1 个(保留开头的单个 /)。
function mergeSlashes(uri) {
return uri.replace(/\\/{2,}/g, "/");
}
function isDocument(uri) {
var lastSeg = uri.slice(uri.lastIndexOf("/") + 1);
// 文档:目录请求(结尾 /)、无扩展名的 SPA 路由、或显式 index.html
return lastSeg === "" || lastSeg === "index.html" || !lastSeg.includes(".");
}
function handler(event) {
var request = event.request;
var host = request.headers["host"] ? request.headers["host"].value : "";
// 先做斜杠归一,再判定文档/资源;无论改写与否,回源到 S3 的 URI 都是归一后的。
request.uri = mergeSlashes(request.uri);
if (isDocument(request.uri)) {
request.uri = "/" + pickFolder(host) + "/index.html";
}
return request;
}
`;
}
/* ───────────────────────── 4. 主流程 ───────────────────────── */
function main() {
if (!fs.existsSync(LOADM)) {
console.error("[generate-sites] 找不到 " + LOADM);
process.exit(1);
}
if (!fs.existsSync(TEMPLATE)) {
console.warn("[generate-sites] 跳过:未找到模板 " + TEMPLATE + "(请先执行 craco build)");
process.exit(0);
}
const src = fs.readFileSync(LOADM, "utf8");
const { sites_config, regions, DEFAULT_REGION } = extractConfig(src);
const hostAlias = extractHostAlias(src);
const template = fs.readFileSync(TEMPLATE, "utf8");
// folder → 规范域名:优先取 HOST_ALIAS 的反向映射,否则默认 .com(og:url / og:image 用)。
const reverseAlias = {};
for (const domain in hostAlias) {
const folder = hostAlias[domain];
if (!reverseAlias[folder]) reverseAlias[folder] = domain;
}
const domainFor = (key) => reverseAlias[key] || key + ".com";
const keys = Object.keys(sites_config);
let count = 0;
for (const key of keys) {
const conf = sites_config[key];
const map = buildReplacements(key, conf, regions, DEFAULT_REGION, domainFor);
const html = applyReplacements(template, map);
const dir = path.join(OUT_DIR, key);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, "index.html"), html);
count++;
}
// 生成 CloudFront Function(host → folder)
const cf = generateCloudFrontFn(keys, hostAlias);
fs.writeFileSync(CF_OUT, cf);
console.log(
"[generate-sites] 已生成 " +
count +
" 个品牌首页 -> build//index.html;CloudFront Function -> " +
path.relative(ROOT, CF_OUT)
);
}
main();