generate-sites.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. /* eslint-disable */
  2. /**
  3. * 多品牌静态首页生成器(构建后处理)
  4. * ============================================================
  5. * 目的:
  6. * 一套 React 产物(build/)服务于 sites_config 中的 N 个品牌站点。
  7. * 首页 index.html 里的 SEO/分享元信息(<title>、description、og:* 等)
  8. * 原本是 %%占位符%%,靠 load.m.js 在浏览器里运行时按 hostname 改写。
  9. * 但爬虫 / 分享卡片抓取时往往不执行 JS,拿到的还是占位符。
  10. *
  11. * 本脚本在 `craco build` + hash-loadm 之后运行:
  12. * 1) 从 public/js/load.m.js 提取 sites_config / regions / HOST_ALIAS(单一数据源,不复制配置)。
  13. * 2) 以 build/index.html(已含带 hash 的资源引用)为模板,
  14. * 逐站点把 %%占位符%% 替换成该品牌的真实内容,写出 build/<folder>/index.html。
  15. * 3) 顺带生成配套的 CloudFront Function(host → folder,运行时 cloudfront-js-2.0),
  16. * 写到项目根 cloudfront-host-to-folder.js,逻辑与 load.m.js 的 getSiteConfig()
  17. * 路由保持一致,保证「静态首页」与「运行时」选到同一个品牌。
  18. *
  19. * folder(文件夹名)= sites_config 的 key = 域名主名(second-level label)。
  20. * 例:mintpayouts.com → folder "mintpayouts";usslot777.com → 走 HOST_ALIAS → "default"。
  21. *
  22. * 用法:node scripts/generate-sites.js (已挂到 package.json 的 build 脚本)
  23. */
  24. const fs = require("fs");
  25. const path = require("path");
  26. const ROOT = path.resolve(__dirname, "..");
  27. // 配置单一数据源:public/js/load.m.js(不在别处复制一份)。
  28. const LOADM = path.join(ROOT, "public", "js", "load.m.js");
  29. // 模板与输出都在 build/:它已被 craco/hash 处理过(带 hash 的 load.m.js / main.js / css)。
  30. const TEMPLATE = path.join(ROOT, "build", "index.html");
  31. const OUT_DIR = path.join(ROOT, "build");
  32. // CloudFront Function 生成路径(在项目根,不在 build/ 内,避免被下次 build 清空)。
  33. const CF_OUT = path.join(ROOT, "cloudfront-host-to-folder.js");
  34. // 与 load.m.js applyPageMeta() 保持一致的标题后缀(运行时也用这个,避免标题闪烁)。
  35. const TITLE_SUFFIX = "Online Slots & Real Money Games";
  36. /* ───────────────────────── 1. 提取 load.m.js 中的配置 ───────────────────────── */
  37. /**
  38. * sites_config / regions / themes / res_path 都是 IIFE 内的私有 const,无法 require。
  39. * 但从 `const res_path` 到 `const getPathPrefix`(第一个用到 window/document 的函数)之间,
  40. * 全是纯数据声明(res_path、各主题对象、regions、DEFAULT_REGION、sites_config),
  41. * 不依赖任何浏览器 API。整段切出来用 new Function 求值即可,无需脆弱的正则解析对象字面量。
  42. */
  43. function extractConfig(src) {
  44. const start = src.indexOf("const res_path");
  45. const end = src.indexOf("const getPathPrefix");
  46. if (start === -1 || end === -1 || end <= start) {
  47. throw new Error("[generate-sites] 无法在 load.m.js 中定位配置数据块(res_path … getPathPrefix)");
  48. }
  49. const block = src.slice(start, end);
  50. // eslint-disable-next-line no-new-func
  51. const fn = new Function(block + "\nreturn { res_path, sites_config, regions, DEFAULT_REGION };");
  52. return fn();
  53. }
  54. /**
  55. * HOST_ALIAS 在 getSiteConfig() 内部,单独用正则切出对象字面量再求值。
  56. * 该字面量没有嵌套大括号,第一个 `}` 即结束。允许内部含注释/尾逗号(new Function 能解析)。
  57. */
  58. function extractHostAlias(src) {
  59. const m = src.match(/const\s+HOST_ALIAS\s*=\s*(\{[\s\S]*?\})\s*;/);
  60. if (!m) return {};
  61. try {
  62. // eslint-disable-next-line no-new-func
  63. return new Function("return (" + m[1] + ");")();
  64. } catch (e) {
  65. console.warn("[generate-sites] 解析 HOST_ALIAS 失败,按空表处理:" + e.message);
  66. return {};
  67. }
  68. }
  69. /* ───────────────────────── 2. 占位符替换 ───────────────────────── */
  70. const escAttr = (s) =>
  71. String(s == null ? "" : s)
  72. .replace(/&/g, "&amp;")
  73. .replace(/</g, "&lt;")
  74. .replace(/>/g, "&gt;")
  75. .replace(/"/g, "&quot;");
  76. function buildReplacements(key, conf, regions, DEFAULT_REGION, domainFor) {
  77. const region = regions[conf.region || DEFAULT_REGION] || regions[DEFAULT_REGION] || {};
  78. const suffix = region.titleSuffix || TITLE_SUFFIX; // 运行时 applyPageMeta 用固定后缀;如需按区域可改 region.titleSuffix
  79. const fullTitle = `${conf.title} | ${suffix}`;
  80. const domain = domainFor(key);
  81. const origin = `https://${domain}`;
  82. const ogImage = conf.icon ? origin + conf.icon : "";
  83. // theme-color:用各主题共有的深色背景(与首屏 loading 背景一致),运行时 setThemeColor 也会接管。
  84. const themeColor = (conf.theme && (conf.theme.bodyBgColor || conf.theme.mainBgColor)) || "#0d1016";
  85. return {
  86. "%%BRAND_TITLE%%": escAttr(fullTitle),
  87. "%%BRAND_DESCRIPTION%%": escAttr(conf.subtitle),
  88. "%%THEME_COLOR%%": escAttr(themeColor),
  89. "%%BRAND_NAME%%": escAttr(conf.title),
  90. "%%BRAND_OG_TITLE%%": escAttr(fullTitle),
  91. "%%BRAND_OG_DESCRIPTION%%": escAttr(conf.subtitle),
  92. "%%BRAND_OG_IMAGE%%": escAttr(ogImage),
  93. "%%BRAND_URL%%": escAttr(origin + "/"),
  94. "%%FAV_ICO%%": escAttr(conf.favicon || "/resources/favicons/favicon.ico"),
  95. };
  96. }
  97. function applyReplacements(template, map) {
  98. let html = template;
  99. for (const token in map) {
  100. html = html.split(token).join(map[token]);
  101. }
  102. return html;
  103. }
  104. /* ───────────────────────── 3. CloudFront Function 生成 ───────────────────────── */
  105. /**
  106. * 生成 host → folder 的 CloudFront Function(viewer-request,运行时 cloudfront-js-2.0)。
  107. * 逻辑对齐 load.m.js getSiteConfig():去 www → HOST_ALIAS → 否则取首段标签 → 校验 folder 是否存在 → 否则 default。
  108. * 仅改写「文档请求」("/"、扩展名为空的 SPA 路由、index.html);静态资源(带扩展名)保持原路径,从根目录回源。
  109. */
  110. function generateCloudFrontFn(folders, hostAlias) {
  111. const foldersMap = {};
  112. folders.forEach((f) => (foldersMap[f] = true));
  113. const foldersJson = JSON.stringify(foldersMap, null, 2);
  114. const aliasJson = JSON.stringify(hostAlias, null, 2);
  115. return `/* eslint-disable */
  116. /**
  117. * CloudFront Function (viewer-request) —— host → 品牌文件夹路由
  118. * ============================================================
  119. * 由 scripts/generate-sites.js 自动生成,请勿手改(改 sites_config 后重跑 npm run build)。
  120. *
  121. * 作用:按访问域名把「文档请求」改写到 /<folder>/index.html,命中对应品牌的静态首页。
  122. * 静态资源(带扩展名,如 .js/.css/.webp/.ico)原样透传,从桶根目录回源。
  123. * 回源到 S3 前先把 URI 里连续的 // 合并为 /(等价 nginx merge_slashes on),
  124. * 避免 //a//b.js 这类多斜杠路径在 S3 里 key 不匹配而 404(S3 不做斜杠归一)。
  125. * 运行时:cloudfront-js-2.0。
  126. * 关联:与 public/js/load.m.js 的 getSiteConfig() 路由保持一致。
  127. */
  128. var FOLDERS = ${foldersJson};
  129. var HOST_ALIAS = ${aliasJson};
  130. function pickFolder(rawHost) {
  131. var host = (rawHost || "").toLowerCase();
  132. if (host.startsWith("www.")) host = host.slice(4);
  133. var colon = host.indexOf(":");
  134. if (colon !== -1) host = host.slice(0, colon);
  135. // 首段标签对应 load.m.js 的 getSitePrefix();整域名例外走 HOST_ALIAS。
  136. var folder = HOST_ALIAS[host] || host.split(".")[0];
  137. return FOLDERS[folder] ? folder : "default";
  138. }
  139. // nginx merge_slashes:把路径里 2 个及以上连续的 / 压成 1 个(保留开头的单个 /)。
  140. function mergeSlashes(uri) {
  141. return uri.replace(/\\/{2,}/g, "/");
  142. }
  143. function isDocument(uri) {
  144. var lastSeg = uri.slice(uri.lastIndexOf("/") + 1);
  145. // 文档:目录请求(结尾 /)、无扩展名的 SPA 路由、或显式 index.html
  146. return lastSeg === "" || lastSeg === "index.html" || !lastSeg.includes(".");
  147. }
  148. function handler(event) {
  149. var request = event.request;
  150. var host = request.headers["host"] ? request.headers["host"].value : "";
  151. // 先做斜杠归一,再判定文档/资源;无论改写与否,回源到 S3 的 URI 都是归一后的。
  152. request.uri = mergeSlashes(request.uri);
  153. if (isDocument(request.uri)) {
  154. request.uri = "/" + pickFolder(host) + "/index.html";
  155. }
  156. return request;
  157. }
  158. `;
  159. }
  160. /* ───────────────────────── 4. 主流程 ───────────────────────── */
  161. function main() {
  162. if (!fs.existsSync(LOADM)) {
  163. console.error("[generate-sites] 找不到 " + LOADM);
  164. process.exit(1);
  165. }
  166. if (!fs.existsSync(TEMPLATE)) {
  167. console.warn("[generate-sites] 跳过:未找到模板 " + TEMPLATE + "(请先执行 craco build)");
  168. process.exit(0);
  169. }
  170. const src = fs.readFileSync(LOADM, "utf8");
  171. const { sites_config, regions, DEFAULT_REGION } = extractConfig(src);
  172. const hostAlias = extractHostAlias(src);
  173. const template = fs.readFileSync(TEMPLATE, "utf8");
  174. // folder → 规范域名:优先取 HOST_ALIAS 的反向映射,否则默认 <key>.com(og:url / og:image 用)。
  175. const reverseAlias = {};
  176. for (const domain in hostAlias) {
  177. const folder = hostAlias[domain];
  178. if (!reverseAlias[folder]) reverseAlias[folder] = domain;
  179. }
  180. const domainFor = (key) => reverseAlias[key] || key + ".com";
  181. const keys = Object.keys(sites_config);
  182. let count = 0;
  183. for (const key of keys) {
  184. const conf = sites_config[key];
  185. const map = buildReplacements(key, conf, regions, DEFAULT_REGION, domainFor);
  186. const html = applyReplacements(template, map);
  187. const dir = path.join(OUT_DIR, key);
  188. fs.mkdirSync(dir, { recursive: true });
  189. fs.writeFileSync(path.join(dir, "index.html"), html);
  190. count++;
  191. }
  192. // 生成 CloudFront Function(host → folder)
  193. const cf = generateCloudFrontFn(keys, hostAlias);
  194. fs.writeFileSync(CF_OUT, cf);
  195. console.log(
  196. "[generate-sites] 已生成 " +
  197. count +
  198. " 个品牌首页 -> build/<folder>/index.html;CloudFront Function -> " +
  199. path.relative(ROOT, CF_OUT)
  200. );
  201. }
  202. main();