verify-inline-scripts.mjs 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. /**
  2. * 验证**构建产物里的内联脚本语法正确** —— 守的是一个真实的盲区。
  3. *
  4. * 为什么需要它(2026-09-24 实际踩到):`npm run build` **不会解析** index.html 里那些普通
  5. * `<script>`(没有 type=module 的内联块),Vite 原样拷过去 —— 于是删代码时删出个不配对的
  6. * `}`,构建照样绿,但页面一加载内联脚本就抛语法错误:`globalThis.hasSequence` 没置位、
  7. * `sequenceLoaded` 没派发 → **加载遮罩永远不隐藏**,用户看到的就是一片"正在初始化,请稍候..."。
  8. *
  9. * 所以这条检查盯的是**产物**(dist/*.html):那里 `%VITE_*%` 已被替换成真值,
  10. * 拿到的就是浏览器真正会执行的那段代码(源码里的 redirect.html 带占位符,本来就不是合法 JS,
  11. * 不能用它做检查)。
  12. *
  13. * 怎么跑:
  14. * npm run build && node harness/tools/verify-inline-scripts.mjs
  15. */
  16. import { existsSync, readFileSync, readdirSync } from 'node:fs';
  17. import { basename, join } from 'node:path';
  18. import vm from 'node:vm';
  19. const DIST = 'dist';
  20. let pass = 0;
  21. let fail = 0;
  22. const check = (name, ok, extra = '') => {
  23. if (ok) {
  24. pass++;
  25. console.log(' ok ' + name);
  26. } else {
  27. fail++;
  28. console.log(' FAIL ' + name + ' ' + extra);
  29. }
  30. };
  31. console.log('\n【1】dist 里的内联脚本能通过语法检查');
  32. if (!existsSync(DIST)) {
  33. console.log(' ⏭ 没有 dist/ —— 先跑 `npm run build`');
  34. process.exit(2);
  35. }
  36. const htmlFiles = readdirSync(DIST).filter((f) => f.endsWith('.html'));
  37. check(`dist 下有 html 产物(${htmlFiles.join(', ')})`, htmlFiles.length > 0);
  38. /** 抽出内联 `<script>`(跳过带 src= 的) */
  39. const extractInlineScripts = (html) =>
  40. [...html.matchAll(/<script(?![^>]*\bsrc=)[^>]*>([\s\S]*?)<\/script>/gi)].map((m) => m[1]);
  41. for (const file of htmlFiles) {
  42. const html = readFileSync(join(DIST, file), 'utf8');
  43. const blocks = extractInlineScripts(html);
  44. if (!blocks.length) {
  45. console.log(` ⏭ ${file} 没有内联脚本`);
  46. continue;
  47. }
  48. blocks.forEach((code, index) => {
  49. const label = `${file} 的第 ${index + 1} 块内联脚本`;
  50. try {
  51. // new vm.Script 只编译不执行 —— 语法错就抛,正好用来当检查
  52. new vm.Script(code);
  53. check(`${label} 语法正确(${code.length} 字符)`, true);
  54. } catch (error) {
  55. const message = error instanceof Error ? error.message : String(error);
  56. check(`${label} 语法正确`, false, message);
  57. }
  58. });
  59. }
  60. console.log('\n【2】本仓库这次踩过的具体症状:占位符没被替换');
  61. {
  62. // 产物里出现未替换的 `%VITE_*%` 说明那个变量没配 —— 轻则功能失效,重则像
  63. // redirect.html 的 redirectMap 一样直接语法错误。这里顺带把它们报出来。
  64. const leftovers = [];
  65. for (const file of htmlFiles) {
  66. const html = readFileSync(join(DIST, file), 'utf8');
  67. for (const m of html.matchAll(/%VITE_[A-Z0-9_]+%/g)) leftovers.push(`${file}: ${m[0]}`);
  68. }
  69. // VITE_DMS_API 在生产构建里本来就未配置(见技术债 #9),单独提示、不算失败
  70. const unexpected = leftovers.filter((item) => !item.includes('%VITE_DMS_API%'));
  71. check('产物里没有被漏替换的 VITE_ 占位符(VITE_DMS_API 单独见技术债 #9)',
  72. unexpected.length === 0, unexpected.join(' | '));
  73. const dmsLeftover = leftovers.filter((item) => item.includes('%VITE_DMS_API%'));
  74. if (dmsLeftover.length) {
  75. console.log(` ℹ️ ${dmsLeftover.join(' | ')} —— 生产未配 DMS,属已知项(技术债 #9)`);
  76. }
  77. }
  78. console.log('\n【3】加载遮罩的**收尾信号**必须还在(少了它页面永远卡在"正在初始化")');
  79. {
  80. // 契约:BusinessAssistant.vue 的 initSequence() 靠 `globalThis.hasSequence`
  81. // (或等 `sequenceLoaded` 事件)隐藏 #loading-cover。
  82. // ⚠️ 2026-09-28 真实踩过:删 index.html 里一段死代码(`if (isKnowledgeApi) { 硬编码 cardInfo }`)时,
  83. // 把**紧跟在它后面的**这三行+addVersionInfo 一起删掉了 → 页面永久卡在加载遮罩。
  84. // (当时的 inline-script 语法检查是全绿的 —— 语法没错,是**语义丢了**。)
  85. const indexHtml = readFileSync(join(DIST, 'index.html'), 'utf8');
  86. check('设置了 globalThis.hasSequence(遮罩收尾判据)', /globalThis\.hasSequence\s*=\s*true/.test(indexHtml));
  87. check('派发了 sequenceLoaded 事件(另一条收尾路径)',
  88. /dispatchEvent\(new CustomEvent\("sequenceLoaded"\)\)/.test(indexHtml));
  89. check('addVersionInfo() 仍被调用(标题带版本号)', /addVersionInfo\(\)/.test(indexHtml));
  90. }
  91. console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`);
  92. process.exit(fail ? 1 : 0);