check.mjs 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. #!/usr/bin/env node
  2. /**
  3. * scripts/check.mjs —— examples/core-wc 静态体检。
  4. *
  5. * 断言五件事(对应规则仓 core-library / web-component-library 的红线):
  6. * 1. core/ 零浏览器依赖、不 import lit;
  7. * 2. wc/ 无装饰器;
  8. * 3. wc/ 组件必须用 Lit 实现(extends LitElement + import 'lit',禁原生 wc 手写);
  9. * 4. 全仓 .js 语法通过 + 相对 import 都带 .js;
  10. * 5. customElements.define 注册幂等。
  11. *
  12. * 用法:node scripts/check.mjs
  13. */
  14. import { readdirSync, readFileSync, statSync } from 'node:fs';
  15. import { join, dirname } from 'node:path';
  16. import { fileURLToPath } from 'node:url';
  17. import { execFileSync } from 'node:child_process';
  18. const root = join(dirname(fileURLToPath(import.meta.url)), '..');
  19. let fail = 0;
  20. const check = (name, ok, detail = '') => {
  21. console.log(`${ok ? '✓' : '✗'} ${name}${!ok && detail ? `\n ${detail}` : ''}`);
  22. if (!ok) fail++;
  23. };
  24. function walk(dir, out = []) {
  25. for (const e of readdirSync(dir, { withFileTypes: true })) {
  26. if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
  27. const p = join(dir, e.name);
  28. if (e.isDirectory()) walk(p, out);
  29. else if (p.endsWith('.js')) out.push(p);
  30. }
  31. return out;
  32. }
  33. const files = walk(root);
  34. /** 去掉注释与字符串字面量,避免把文档里提到的 window/fetch 误判为代码。 */
  35. function strip(src) {
  36. return src
  37. .replace(/\/\*[\s\S]*?\*\//g, '') // 块注释
  38. .replace(/(^|[^:])\/\/[^\n]*/g, '$1'); // 行注释(避开 https://)
  39. }
  40. /** 只去掉注释(保留字符串),用于「是否 import 某包」这类**看字面量**的判定。 */
  41. function stripComments(src) {
  42. return src
  43. .replace(/\/\*[\s\S]*?\*\//g, '')
  44. .replace(/(^|[^:])\/\/[^\n]*/g, '$1');
  45. }
  46. // 1. core 零浏览器依赖
  47. const DOM_GLOBALS = /\b(window|document|localStorage|sessionStorage|navigator|HTMLElement|CustomEvent)\b|\bfetch\s*\(/;
  48. const coreFiles = files.filter((f) => f.includes('/core/'));
  49. check('core/ 目录非空', coreFiles.length > 0);
  50. let domHits = [];
  51. for (const f of coreFiles) {
  52. const src = strip(readFileSync(f, 'utf8'));
  53. const m = src.match(DOM_GLOBALS);
  54. if (m) domHits.push(`${f}: ${m[0]}`);
  55. }
  56. check('core/ 无浏览器全局(window/document/fetch/…)', domHits.length === 0, domHits.join('\n '));
  57. const litHits = coreFiles.filter((f) => /from\s+['"]lit(-html|-element)?['"]/.test(strip(readFileSync(f, 'utf8'))));
  58. check('core/ 不 import lit', litHits.length === 0, litHits.join('\n '));
  59. // 2. wc 无装饰器
  60. const DECORATOR = /^\s*@(customElement|property|state|consume|provide|query|queryAssignedElements)\b/m;
  61. const decoHits = files.filter((f) => DECORATOR.test(strip(readFileSync(f, 'utf8'))));
  62. check('全仓无装饰器语法', decoHits.length === 0, decoHits.join('\n '));
  63. // 3. wc/ 必须用 Lit 实现(禁原生 Web Component 手写)
  64. //
  65. // 判定口径:**注册了自定义元素的文件**就是「组件」,必须用 Lit 框架特性开发。
  66. // 不渲染 DOM 的纯 I/O 适配器(如 browser-transport.js,不 define)不在此列。
  67. const NATIVE_WC = [
  68. [/\bextends\s+HTMLElement\b/, 'extends HTMLElement(原生 wc)'],
  69. [/\battachShadow\s*\(/, 'attachShadow()(手写 Shadow DOM)'],
  70. [/\bobservedAttributes\b/, 'observedAttributes(手写属性观察)'],
  71. [/\battributeChangedCallback\b/, 'attributeChangedCallback(手写属性观察)'],
  72. ];
  73. const wcComponentFiles = files.filter((f) => f.includes('/wc/') && /customElements\.define\(/.test(readFileSync(f, 'utf8')));
  74. check('wc/ 存在注册自定义元素的组件文件', wcComponentFiles.length > 0);
  75. const notLit = wcComponentFiles.filter((f) => !/from\s+['"]lit(-element)?['"]/.test(stripComments(readFileSync(f, 'utf8'))));
  76. check('wc/ 组件均 import lit', notLit.length === 0,
  77. notLit.map((f) => `${f}: 缺少 import { LitElement, html, css } from 'lit'`).join('\n '));
  78. const notLitElement = wcComponentFiles.filter((f) => !/\bextends\s+LitElement\b/.test(strip(readFileSync(f, 'utf8'))));
  79. check('wc/ 组件均 extends LitElement', notLitElement.length === 0,
  80. notLitElement.map((f) => `${f}: 组件类必须继承 LitElement(见 web-component-library.md 第〇节)`).join('\n '));
  81. let nativeHits = [];
  82. for (const f of wcComponentFiles) {
  83. const src = strip(readFileSync(f, 'utf8'));
  84. for (const [re, label] of NATIVE_WC) {
  85. if (re.test(src)) nativeHits.push(`${f}: ${label}`);
  86. }
  87. }
  88. check('wc/ 组件无原生 wc 写法(extends HTMLElement / attachShadow / observedAttributes)', nativeHits.length === 0,
  89. nativeHits.join('\n '));
  90. // 4. 语法 + 相对 import 带 .js
  91. let syntaxBad = [];
  92. const suffixBad = [];
  93. for (const f of files) {
  94. try { execFileSync(process.execPath, ['--check', f], { stdio: 'pipe' }); }
  95. catch { syntaxBad.push(f); }
  96. const src = strip(readFileSync(f, 'utf8'));
  97. for (const m of src.matchAll(/\bfrom\s+['"](\.[^'"]*)['"]/g)) {
  98. if (!/\.js$/.test(m[1]) && !/\{\}/.test(m[1])) suffixBad.push(`${f}: ${m[1]}`);
  99. }
  100. // 裸标识符 import(非相对、非 node:)必须能进 importmap
  101. for (const m of src.matchAll(/\bfrom\s+['"]([^.'"][^'"]*)['"]/g)) {
  102. const spec = m[1];
  103. if (spec.startsWith('node:')) continue;
  104. if (!/^(lit|lit-html|lit-element|@lit\/|@lit-labs\/|signal-polyfill|\/)/.test(spec)) {
  105. suffixBad.push(`${f}: 裸标识符 ${spec} 未列入 importmap 白名单`);
  106. }
  107. }
  108. }
  109. check('全部 .js 通过 node --check', syntaxBad.length === 0, syntaxBad.join('\n '));
  110. check('相对 import 带 .js 且裸标识符在 importmap 内', suffixBad.length === 0, suffixBad.join('\n '));
  111. // 5. 注册幂等
  112. const defs = files.filter((f) => /customElements\.define\(/.test(readFileSync(f, 'utf8')));
  113. const guarded = defs.filter((f) => /customElements\.get\(/.test(readFileSync(f, 'utf8')));
  114. check('customElements.define 均有 get() 判重', defs.length > 0 && defs.length === guarded.length,
  115. defs.filter((f) => !guarded.includes(f)).join('\n '));
  116. console.log(fail === 0 ? '\nCHECK OK' : `\nCHECK FAILED(${fail})`);
  117. process.exit(fail === 0 ? 0 : 1);