config.mjs 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. #!/usr/bin/env node
  2. /**
  3. * scripts/config.mjs —— 部署前缀的**唯一来源**,并把同一份配置写进三个地方。
  4. *
  5. * 三处同源(pitfalls.md 第五条 / routing-best-practices.md 坑 4):
  6. * 1. <base href> ← index.html
  7. * 2. importmap 里的前缀 ← index.html(依赖 + 业务路径都在前缀下)
  8. * 3. OBS / vendor 上传前缀 ← deploy.sh 读同一个 prefix 文件
  9. *
  10. * 做法:先写 deploy/prefix/<env>.txt,再写 index.html。所有副本都由 prefix 派生,
  11. * 不存在「手工改了其中一处」的可能。check.mjs 会反向校验三者一致。
  12. *
  13. * 用法:node scripts/config.mjs <env> [--base <url>]
  14. * node scripts/config.mjs local # http://localhost:<port>/ (由 serve.mjs 读取)
  15. * node scripts/config.mjs dev # /kangshifu/dev/ → https://kangshifu.fmode.cn/kangshifu/dev/
  16. * node scripts/config.mjs prod # /kangshifu/ → https://kangshifu.fmode.cn/kangshifu/
  17. * node scripts/config.mjs --prefix /a/b/ # 自定义前缀(必须以 / 开头、/ 结尾)
  18. *
  19. * 前缀可以是**完整 URL**(如 https://cdn.example.com/x/env/proj/):这时所有模块都以绝对 URL 加载,
  20. * 与页面所在的位置无关 —— 本地用 http.server 按同一目录结构起服务,就能验证「子路径部署」而不改任何源码。
  21. */
  22. import { writeFileSync, mkdirSync, readFileSync } from 'node:fs';
  23. import { join, dirname } from 'node:path';
  24. import { fileURLToPath } from 'node:url';
  25. export const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
  26. const PREFIX_DIR = join(ROOT, 'deploy/prefix');
  27. /** 环境 → 部署前缀。改部署位置只改这里。 */
  28. export const ENVS = {
  29. local: '/',
  30. // 本地验证「子路径部署」用:同一套静态文件挂在二级路径下
  31. 'local-sub': '/kangshifu/dev/inno-board/',
  32. dev: '/kangshifu/dev/',
  33. prod: '/kangshifu/',
  34. };
  35. export function normalizePrefix(p) {
  36. if (!p) throw new Error('prefix 不能为空(根部署用 "/")');
  37. const s = String(p);
  38. if (/^https?:\/\//.test(s)) return s.endsWith('/') ? s : s + '/';
  39. if (!s.startsWith('/')) throw new Error(`prefix 必须以 / 开头:${s}`);
  40. return s.endsWith('/') ? s : s + '/';
  41. }
  42. /** 在 `<base>` 之后按前缀生成 importmap 原文。依赖与业务都挂在前缀下(同源,无 CORS)。 */
  43. export function importMapJson(prefix) {
  44. const j = (p) => JSON.stringify(prefix + p);
  45. return `{
  46. "imports": {
  47. "lit": ${j('lib/vendor/lit/v3.3.3/index.js')},
  48. "lit/": ${j('lib/vendor/lit/v3.3.3/')},
  49. "lit-html": ${j('lib/vendor/lit-html/v3.3.3/lit-html.js')},
  50. "lit-html/": ${j('lib/vendor/lit-html/v3.3.3/')},
  51. "lit-element": ${j('lib/vendor/lit-element/v4.2.2/index.js')},
  52. "lit-element/": ${j('lib/vendor/lit-element/v4.2.2/')},
  53. "@lit/reactive-element": ${j('lib/vendor/@lit/reactive-element/v2.1.2/reactive-element.js')},
  54. "@lit/reactive-element/": ${j('lib/vendor/@lit/reactive-element/v2.1.2/')},
  55. "@lit/context": ${j('lib/vendor/@lit/context/v1.1.6/index.js')},
  56. "@lit/context/": ${j('lib/vendor/@lit/context/v1.1.6/')},
  57. "@lit-labs/signals": ${j('lib/vendor/@lit-labs/signals/v0.3.0/index.js')},
  58. "@lit-labs/signals/": ${j('lib/vendor/@lit-labs/signals/v0.3.0/')},
  59. "signal-polyfill": ${j('lib/vendor/signal-polyfill/v0.2.2/dist/index.js')},
  60. "signal-polyfill/": ${j('lib/vendor/signal-polyfill/v0.2.2/dist/')}
  61. }
  62. }`;
  63. }
  64. /** index.html 原文:base href、importmap、模块入口全部由 prefix 派生。 */
  65. export function indexHtml(prefix, { dev = false } = {}) {
  66. return `<!doctype html>
  67. <html lang="zh-CN">
  68. <head>
  69. <meta charset="utf-8" />
  70. <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
  71. <meta name="color-scheme" content="light" />
  72. <meta name="theme-color" content="#e9edf7" />
  73. <title>康师傅 · 产品创新看板</title>
  74. <meta name="description" content="康师傅研发中心 AI 赋能实战营 · 多人工作台 + 机会卡片池" />
  75. <!-- 部署前缀唯一来源:scripts/config.mjs(与 importmap 前缀、上传前缀同源) -->
  76. <base href="${prefix}" />
  77. <link rel="preload" href="${prefix}src/assets/inter-latin.woff2" as="font" type="font/woff2" crossorigin />
  78. <script type="importmap">
  79. ${importMapJson(prefix)}
  80. </script>
  81. <script>${dev ? 'window.__KF_DEV__=true;window.__KF_PARSE_BASE__="https://kangshifu.fmode.cn/parse";' : ''}</script>
  82. <script type="module" src="${prefix}src/app.js"></script>
  83. </head>
  84. <body>
  85. <kf-app></kf-app>
  86. <noscript><p style="padding:24px;font:16px system-ui">这个看板需要 JavaScript 才能读取数据。请在浏览器中启用 JavaScript 后刷新。</p></noscript>
  87. </body>
  88. </html>
  89. `;
  90. }
  91. /** 写出 index.html + 前缀文件,并自检三处同源。部署脚本与 verify 共用这一个入口。 */
  92. export function writeConfig({ env = 'local', prefix: override = null } = {}) {
  93. let prefix;
  94. if (override) {
  95. prefix = normalizePrefix(override);
  96. env = env || 'custom';
  97. } else {
  98. env = env || 'local';
  99. if (!ENVS[env]) throw new Error(`未知环境 ${env};可选:${Object.keys(ENVS).join(' / ')},或 --prefix <路径>`);
  100. prefix = ENVS[env];
  101. }
  102. const dev = env === 'local' || env === 'local-sub';
  103. writeFileSync(join(ROOT, 'index.html'), indexHtml(prefix, { dev }));
  104. mkdirSync(PREFIX_DIR, { recursive: true });
  105. writeFileSync(join(PREFIX_DIR, `${env}.txt`), prefix + '\n');
  106. // 自检:写完后反向读取,三处必须同源
  107. const html = readFileSync(join(ROOT, 'index.html'), 'utf8');
  108. const base = html.match(/<base href="([^"]+)"/)?.[1];
  109. const map = JSON.parse(html.match(/<script type="importmap">([\s\S]*?)<\/script>/)[1]);
  110. const stored = readFileSync(join(PREFIX_DIR, `${env}.txt`), 'utf8').trim();
  111. const ok = base === prefix && stored === prefix &&
  112. Object.values(map.imports).every((v) => v.startsWith(prefix)) &&
  113. html.includes(`src="${prefix}src/app.js"`);
  114. if (!ok) throw new Error(`三处不同源:base=${base} stored=${stored} prefix=${prefix}`);
  115. return { env, prefix, base, stored, importCount: Object.keys(map.imports).length };
  116. }
  117. function main() {
  118. const args = process.argv.slice(2);
  119. const envArg = args.find((a) => !a.startsWith('--'));
  120. const override = args.includes('--prefix') ? args[args.indexOf('--prefix') + 1] : null;
  121. const { env, prefix, base, stored, importCount } = writeConfig({ env: envArg, prefix: override });
  122. const firstImport = JSON.parse(readFileSync(join(ROOT, 'index.html'), 'utf8')
  123. .match(/<script type="importmap">([\s\S]*?)<\/script>/)[1]).imports.lit;
  124. console.log(`✓ env=${env} prefix=${prefix}`);
  125. console.log(` base href = ${base}`);
  126. console.log(` importmap 前缀 = ${firstImport.replace(/lib\/vendor.*$/, '')}(${importCount} 条映射)`);
  127. console.log(` deploy/prefix/${env}.txt = ${stored}`);
  128. }
  129. if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) main();