| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- /* eslint-env node */
- /**
- * 多品牌静态首页生成器(构建后处理)
- * ============================================================
- * 目的:
- * 一套 React 产物(build/)服务于 sites_config 中的 N 个品牌站点。
- * 首页 index.html 里的 SEO/分享元信息(<title>、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/<folder>/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, ">")
- .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)。
- *
- * 作用:按访问域名把「文档请求」改写到 /<folder>/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 的反向映射,否则默认 <key>.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/<folder>/index.html;CloudFront Function -> " +
- path.relative(ROOT, CF_OUT)
- );
- }
- main();
|