verify-vue-script-html-comments.mjs 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. /**
  2. * 验证:`.vue` 的 `<script>` 里**没有裸的 HTML 注释起始符**。
  3. *
  4. * 为什么要有这条(2026-09-28 实际踩过):
  5. * Vite 的依赖预扫描把 **`.vue` 也当 HTML**(`htmlTypesRE = /\.(html|vue|svelte|astro|imba)$/`),
  6. * 扫描前会先做一次 `raw.replace(/<!--.*?-->/gs, "<!---->")`(带 `s`,**可跨行**)。
  7. * 于是 script 里一个**不成对的** `<!--`,会和**后面任意一个 `-->`**(哪怕隔着几十行)
  8. * 配成"HTML 注释"被整段吃掉 —— 行数因此变少、后面的代码可能并进上一行的 `//` 注释,
  9. * esbuild 随即报 `Unexpected "."`,整个依赖扫描失败。
  10. *
  11. * 症状特点(所以必须单独有个脚本盯着):
  12. * - **`npm run build` 抓不到**:构建走 vue 插件路径,不做这步清扫
  13. * - **平时启动也看不到**:只在依赖缓存失效、真正重新扫描时炸一次(改 package.json/lock、
  14. * `npx vite --force`、换分支等)
  15. * - **`<template>` 里的 `<!-- … -->` 是合法的**,本脚本只查 `<script>` 区块,
  16. * 且只报"同行没有配对 `-->`"的那些(同行配对的会被替换成 `<!---->`,无害)
  17. *
  18. * 怎么跑(纯 node,不需要打包):
  19. * node harness/tools/verify-vue-script-html-comments.mjs
  20. * 退出码:0 = 干净;1 = 有裸起始符(会打印 file:line)
  21. */
  22. import { readdirSync, readFileSync, statSync } from 'node:fs';
  23. import { join } from 'node:path';
  24. const OPEN = '<!-' + '-';
  25. const CLOSE = '--' + '>';
  26. const SCRIPT_OPEN_RE = /<script\b[^>]*>/g;
  27. const SCRIPT_CLOSE = '</' + 'script>';
  28. const SKIP_DIRS = new Set(['node_modules', 'dist', '.git', 'harness']);
  29. /** 收集 src 下所有 .vue */
  30. const collectVueFiles = (dir) => {
  31. const out = [];
  32. for (const entry of readdirSync(dir)) {
  33. if (SKIP_DIRS.has(entry)) continue;
  34. const full = join(dir, entry);
  35. if (statSync(full).isDirectory()) out.push(...collectVueFiles(full));
  36. else if (entry.endsWith('.vue')) out.push(full);
  37. }
  38. return out;
  39. };
  40. const files = collectVueFiles('src');
  41. const offenders = [];
  42. let pairedButHarmless = 0;
  43. for (const file of files) {
  44. const text = readFileSync(file, 'utf8');
  45. // 1) 找出所有 <script> 区块的字符区间
  46. const ranges = [];
  47. SCRIPT_OPEN_RE.lastIndex = 0;
  48. let m;
  49. while ((m = SCRIPT_OPEN_RE.exec(text))) {
  50. const start = m.index + m[0].length;
  51. const end = text.indexOf(SCRIPT_CLOSE, start);
  52. ranges.push([start, end === -1 ? text.length : end]);
  53. }
  54. if (!ranges.length) continue;
  55. // 2) 在 script 区间内逐行找裸起始符
  56. let offset = 0;
  57. const lines = text.split('\n');
  58. for (let i = 0; i < lines.length; i += 1) {
  59. const line = lines[i];
  60. let idx = line.indexOf(OPEN);
  61. while (idx !== -1) {
  62. const abs = offset + idx;
  63. const inScript = ranges.some(([a, b]) => abs >= a && abs < b);
  64. if (inScript) {
  65. if (line.indexOf(CLOSE, idx + OPEN.length) === -1) {
  66. offenders.push({ file, line: i + 1, text: line.trim().slice(0, 100) });
  67. } else {
  68. pairedButHarmless += 1;
  69. }
  70. }
  71. idx = line.indexOf(OPEN, idx + OPEN.length);
  72. }
  73. offset += line.length + 1;
  74. }
  75. }
  76. console.log(`扫描 ${files.length} 个 .vue 文件的 <script> 区块`);
  77. console.log(` 同行已配对的起始符(会被替换成 <!---->,无害):${pairedButHarmless} 处`);
  78. if (offenders.length) {
  79. console.log(`\n❌ 发现 ${offenders.length} 处裸起始符 —— 会让 Vite 的依赖扫描吃掉后面的代码:`);
  80. for (const o of offenders) console.log(` ${o.file}:${o.line} ${o.text}`);
  81. console.log('\n修法:把字面量挪到 .ts(参考 src/utils/stream-xml-filter.ts 的 POLICY_TABLE_START),');
  82. console.log('或改写这段文字,别让它在 .vue 的 script 里出现。');
  83. process.exit(1);
  84. }
  85. console.log('\n✅ 通过:没有裸的 HTML 注释起始符');