check-elements.mjs 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. #!/usr/bin/env node
  2. /**
  3. * check-elements.mjs —— 校验 src/ 下所有自定义元素都显式声明了 display
  4. *
  5. * 背景:light DOM / Shadow DOM 下自定义元素都是「未知元素」,浏览器默认按 inline 处理,
  6. * 不吃 height/width、不形成块级盒子,常见症状是 h-full / flex-1 全失效、高度链断裂。
  7. * 规则见 SKILL.md「多页面开发铁律 · 死线4」。
  8. *
  9. * 用法:
  10. * node scripts/check-elements.mjs [srcDir] # 默认 ./src
  11. *
  12. * 判定:
  13. * 1. 收集源码中 customElements.define('tag', ...) 注册的全部标签;
  14. * 2. 在 .css 文件与内联 <style> 中查找该标签的选择器块;
  15. * 3. 选择器块含 display 声明 → 通过;写 `display: skip` 标记为例外(如自带 flex 布局的元素);
  16. * 4. 未声明 → 报错,退出码 1。
  17. */
  18. import { readdirSync, readFileSync, statSync, existsSync } from 'node:fs';
  19. import { join, extname } from 'node:path';
  20. const SRC = process.argv[2] || 'src';
  21. if (!existsSync(SRC)) {
  22. console.error(`✗ 目录不存在:${SRC}`);
  23. process.exit(2);
  24. }
  25. /** 递归收集文件 */
  26. function walk(dir, out = []) {
  27. for (const name of readdirSync(dir)) {
  28. const p = join(dir, name);
  29. const st = statSync(p);
  30. if (st.isDirectory()) walk(p, out);
  31. else if (/\.(js|mjs|html|css|jsx|tsx?)$/.test(name)) out.push(p);
  32. }
  33. return out;
  34. }
  35. const files = walk(SRC);
  36. const defined = new Set(); // customElements.define 注册的标签
  37. let cssText = '';
  38. for (const f of files) {
  39. const text = readFileSync(f, 'utf8');
  40. for (const m of text.matchAll(/customElements\.define\(\s*['"]([\w-]+)['"]/g)) defined.add(m[1]);
  41. if (extname(f) === '.css') cssText += '\n' + text;
  42. // 内联 <style> 块
  43. for (const m of text.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g)) cssText += '\n' + m[1];
  44. }
  45. /** 在 cssText 中查找某个标签的样式块(tag { ... } 或 tag[attr] { ... }) */
  46. function findRules(tag) {
  47. const re = new RegExp(`(^|[\\s,{}>])${tag}\\s*\\{([^}]*)\\}`, 'gm');
  48. const rules = [];
  49. for (const m of cssText.matchAll(re)) rules.push(m[2]);
  50. return rules;
  51. }
  52. const RED = '\x1b[31m', GREEN = '\x1b[32m', YELLOW = '\x1b[33m', DIM = '\x1b[2m', RESET = '\x1b[0m';
  53. let bad = 0, skipped = 0, ok = 0;
  54. // 只检查「已注册的自定义元素」—— 未知/拼错的标签不归本检查管
  55. const tags = [...defined];
  56. for (const tag of tags.sort()) {
  57. const rules = findRules(tag);
  58. const declared = rules.some((r) => /display\s*:/.test(r));
  59. const exempted = rules.some((r) => /display\s*:\s*skip/.test(r));
  60. if (exempted) { skipped++; console.log(`${YELLOW}-${RESET} ${tag} ${DIM}(例外声明 display: skip)${RESET}`); }
  61. else if (declared) ok++;
  62. else { bad++; console.log(`${RED}✗${RESET} ${tag} 未声明 display — add: ${tag} { display: block; min-width: 0; }`); }
  63. }
  64. console.log(`\n自定义元素 ${tags.length} 个:${GREEN}已声明 ${ok}${RESET},${YELLOW}例外 ${skipped}${RESET},${bad ? RED + '未声明 ' + bad : '未声明 0'}${RESET}`);
  65. console.log(bad ? `${RED}CHECK FAILED${RESET}` : `${DIM}回滚显示/高度链相关白屏时,先看这些元素是否 inline。${RESET}`);
  66. process.exit(bad ? 1 : 0);