check.mjs 3.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  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. 全仓 .js 语法通过 + 相对 import 都带 .js。
  9. *
  10. * 用法:node scripts/check.mjs
  11. */
  12. import { readdirSync, readFileSync, statSync } from 'node:fs';
  13. import { join, dirname } from 'node:path';
  14. import { fileURLToPath } from 'node:url';
  15. import { execFileSync } from 'node:child_process';
  16. const root = join(dirname(fileURLToPath(import.meta.url)), '..');
  17. let fail = 0;
  18. const check = (name, ok, detail = '') => {
  19. console.log(`${ok ? '✓' : '✗'} ${name}${!ok && detail ? `\n ${detail}` : ''}`);
  20. if (!ok) fail++;
  21. };
  22. function walk(dir, out = []) {
  23. for (const e of readdirSync(dir, { withFileTypes: true })) {
  24. if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
  25. const p = join(dir, e.name);
  26. if (e.isDirectory()) walk(p, out);
  27. else if (p.endsWith('.js')) out.push(p);
  28. }
  29. return out;
  30. }
  31. const files = walk(root);
  32. /** 去掉注释与字符串字面量,避免把文档里提到的 window/fetch 误判为代码。 */
  33. function strip(src) {
  34. return src
  35. .replace(/\/\*[\s\S]*?\*\//g, '') // 块注释
  36. .replace(/(^|[^:])\/\/[^\n]*/g, '$1') // 行注释(避开 https://)
  37. .replace(/'(?:[^'\\]|\\.)*'/g, "''") // 单引号串
  38. .replace(/"(?:[^"\\]|\\.)*"/g, '""'); // 双引号串
  39. }
  40. // 1. core 零浏览器依赖
  41. const DOM_GLOBALS = /\b(window|document|localStorage|sessionStorage|navigator|HTMLElement|CustomEvent)\b|\bfetch\s*\(/;
  42. const coreFiles = files.filter((f) => f.includes('/core/'));
  43. check('core/ 目录非空', coreFiles.length > 0);
  44. let domHits = [];
  45. for (const f of coreFiles) {
  46. const src = strip(readFileSync(f, 'utf8'));
  47. const m = src.match(DOM_GLOBALS);
  48. if (m) domHits.push(`${f}: ${m[0]}`);
  49. }
  50. check('core/ 无浏览器全局(window/document/fetch/…)', domHits.length === 0, domHits.join('\n '));
  51. const litHits = coreFiles.filter((f) => /from\s+['"]lit(-html|-element)?['"]/.test(strip(readFileSync(f, 'utf8'))));
  52. check('core/ 不 import lit', litHits.length === 0, litHits.join('\n '));
  53. // 2. wc 无装饰器
  54. const DECORATOR = /^\s*@(customElement|property|state|consume|provide|query|queryAssignedElements)\b/m;
  55. const decoHits = files.filter((f) => DECORATOR.test(strip(readFileSync(f, 'utf8'))));
  56. check('全仓无装饰器语法', decoHits.length === 0, decoHits.join('\n '));
  57. // 3. 语法 + 相对 import 带 .js
  58. let syntaxBad = [];
  59. const suffixBad = [];
  60. for (const f of files) {
  61. try { execFileSync(process.execPath, ['--check', f], { stdio: 'pipe' }); }
  62. catch { syntaxBad.push(f); }
  63. const src = strip(readFileSync(f, 'utf8'));
  64. for (const m of src.matchAll(/\bfrom\s+['"](\.[^'"]*)['"]/g)) {
  65. if (!/\.js$/.test(m[1]) && !/\{\}/.test(m[1])) suffixBad.push(`${f}: ${m[1]}`);
  66. }
  67. // 裸标识符 import(非相对、非 node:)必须能进 importmap
  68. for (const m of src.matchAll(/\bfrom\s+['"]([^.'"][^'"]*)['"]/g)) {
  69. const spec = m[1];
  70. if (spec.startsWith('node:')) continue;
  71. if (!/^(lit|lit-html|lit-element|@lit\/|@lit-labs\/|signal-polyfill|\/)/.test(spec)) {
  72. suffixBad.push(`${f}: 裸标识符 ${spec} 未列入 importmap 白名单`);
  73. }
  74. }
  75. }
  76. check('全部 .js 通过 node --check', syntaxBad.length === 0, syntaxBad.join('\n '));
  77. check('相对 import 带 .js 且裸标识符在 importmap 内', suffixBad.length === 0, suffixBad.join('\n '));
  78. // 4. 注册幂等
  79. const defs = files.filter((f) => /customElements\.define\(/.test(readFileSync(f, 'utf8')));
  80. const guarded = defs.filter((f) => /customElements\.get\(/.test(readFileSync(f, 'utf8')));
  81. check('customElements.define 均有 get() 判重', defs.length > 0 && defs.length === guarded.length,
  82. defs.filter((f) => !guarded.includes(f)).join('\n '));
  83. console.log(fail === 0 ? '\nCHECK OK' : `\nCHECK FAILED(${fail})`);
  84. process.exit(fail === 0 ? 0 : 1);