| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495 |
- #!/usr/bin/env node
- /**
- * scripts/check.mjs —— examples/core-wc 静态体检。
- *
- * 断言三件事(对应规则仓 core-library / web-component-library 的红线):
- * 1. core/ 零浏览器依赖、不 import lit;
- * 2. wc/ 无装饰器;
- * 3. 全仓 .js 语法通过 + 相对 import 都带 .js。
- *
- * 用法:node scripts/check.mjs
- */
- import { readdirSync, readFileSync, statSync } from 'node:fs';
- import { join, dirname } from 'node:path';
- import { fileURLToPath } from 'node:url';
- import { execFileSync } from 'node:child_process';
- const root = join(dirname(fileURLToPath(import.meta.url)), '..');
- let fail = 0;
- const check = (name, ok, detail = '') => {
- console.log(`${ok ? '✓' : '✗'} ${name}${!ok && detail ? `\n ${detail}` : ''}`);
- if (!ok) fail++;
- };
- function walk(dir, out = []) {
- for (const e of readdirSync(dir, { withFileTypes: true })) {
- if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
- const p = join(dir, e.name);
- if (e.isDirectory()) walk(p, out);
- else if (p.endsWith('.js')) out.push(p);
- }
- return out;
- }
- const files = walk(root);
- /** 去掉注释与字符串字面量,避免把文档里提到的 window/fetch 误判为代码。 */
- function strip(src) {
- return src
- .replace(/\/\*[\s\S]*?\*\//g, '') // 块注释
- .replace(/(^|[^:])\/\/[^\n]*/g, '$1') // 行注释(避开 https://)
- .replace(/'(?:[^'\\]|\\.)*'/g, "''") // 单引号串
- .replace(/"(?:[^"\\]|\\.)*"/g, '""'); // 双引号串
- }
- // 1. core 零浏览器依赖
- const DOM_GLOBALS = /\b(window|document|localStorage|sessionStorage|navigator|HTMLElement|CustomEvent)\b|\bfetch\s*\(/;
- const coreFiles = files.filter((f) => f.includes('/core/'));
- check('core/ 目录非空', coreFiles.length > 0);
- let domHits = [];
- for (const f of coreFiles) {
- const src = strip(readFileSync(f, 'utf8'));
- const m = src.match(DOM_GLOBALS);
- if (m) domHits.push(`${f}: ${m[0]}`);
- }
- check('core/ 无浏览器全局(window/document/fetch/…)', domHits.length === 0, domHits.join('\n '));
- const litHits = coreFiles.filter((f) => /from\s+['"]lit(-html|-element)?['"]/.test(strip(readFileSync(f, 'utf8'))));
- check('core/ 不 import lit', litHits.length === 0, litHits.join('\n '));
- // 2. wc 无装饰器
- const DECORATOR = /^\s*@(customElement|property|state|consume|provide|query|queryAssignedElements)\b/m;
- const decoHits = files.filter((f) => DECORATOR.test(strip(readFileSync(f, 'utf8'))));
- check('全仓无装饰器语法', decoHits.length === 0, decoHits.join('\n '));
- // 3. 语法 + 相对 import 带 .js
- let syntaxBad = [];
- const suffixBad = [];
- for (const f of files) {
- try { execFileSync(process.execPath, ['--check', f], { stdio: 'pipe' }); }
- catch { syntaxBad.push(f); }
- const src = strip(readFileSync(f, 'utf8'));
- for (const m of src.matchAll(/\bfrom\s+['"](\.[^'"]*)['"]/g)) {
- if (!/\.js$/.test(m[1]) && !/\{\}/.test(m[1])) suffixBad.push(`${f}: ${m[1]}`);
- }
- // 裸标识符 import(非相对、非 node:)必须能进 importmap
- for (const m of src.matchAll(/\bfrom\s+['"]([^.'"][^'"]*)['"]/g)) {
- const spec = m[1];
- if (spec.startsWith('node:')) continue;
- if (!/^(lit|lit-html|lit-element|@lit\/|@lit-labs\/|signal-polyfill|\/)/.test(spec)) {
- suffixBad.push(`${f}: 裸标识符 ${spec} 未列入 importmap 白名单`);
- }
- }
- }
- check('全部 .js 通过 node --check', syntaxBad.length === 0, syntaxBad.join('\n '));
- check('相对 import 带 .js 且裸标识符在 importmap 内', suffixBad.length === 0, suffixBad.join('\n '));
- // 4. 注册幂等
- const defs = files.filter((f) => /customElements\.define\(/.test(readFileSync(f, 'utf8')));
- const guarded = defs.filter((f) => /customElements\.get\(/.test(readFileSync(f, 'utf8')));
- check('customElements.define 均有 get() 判重', defs.length > 0 && defs.length === guarded.length,
- defs.filter((f) => !guarded.includes(f)).join('\n '));
- console.log(fail === 0 ? '\nCHECK OK' : `\nCHECK FAILED(${fail})`);
- process.exit(fail === 0 ? 0 : 1);
|