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