// 已读取AGENTS.md import * as fs from 'fs'; import * as path from 'path'; /** * 第九十二批:小程序命名导入静态校验。 * 背景:apply.vue 引入未导出的 normalizeUserId,导致 HBuilderX 编译直接失败, * 而本仓库单测不覆盖小程序编译,只能等人工运行才发现。此用例补上静态闸门。 */ describe('第九十二批小程序命名导入校验', () => { const root = path.resolve(__dirname, '../../pwuniapp'); const walk = (dir: string, out: string[]): string[] => { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { if (entry.name === 'node_modules' || entry.name === 'unpackage' || entry.name.startsWith('.')) continue; const full = path.join(dir, entry.name); if (entry.isDirectory()) walk(full, out); else if (/\.(vue|js)$/.test(entry.name)) out.push(full); } return out; }; const exportedNames = (file: string): Set => { const src = fs.readFileSync(file, 'utf-8'); const names = new Set(); const patterns = [ /export\s+(?:async\s+)?function\s+([A-Za-z0-9_$]+)/g, /export\s+(?:const|let|var)\s+([A-Za-z0-9_$]+)/g, /export\s+class\s+([A-Za-z0-9_$]+)/g, ]; for (const re of patterns) { let m: RegExpExecArray | null; while ((m = re.exec(src))) names.add(m[1]); } const braceRe = /export\s*\{([^}]+)\}/g; let b: RegExpExecArray | null; while ((b = braceRe.exec(src))) { for (const part of b[1].split(',')) { const seg = part.trim(); if (!seg) continue; const as = seg.split(/\s+as\s+/); names.add((as[1] || as[0]).trim()); } } if (/export\s+default\b/.test(src)) names.add('default'); if (/export\s+\*/.test(src)) names.add('*'); return names; }; it('所有命名导入都能在目标模块找到导出', () => { const problems: string[] = []; for (const file of walk(root, [])) { const src = fs.readFileSync(file, 'utf-8'); const importRe = /import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g; let m: RegExpExecArray | null; while ((m = importRe.exec(src))) { const spec = m[2]; let target: string | null = null; if (spec.startsWith('@/')) target = path.join(root, spec.slice(2)); else if (spec.startsWith('.')) target = path.resolve(path.dirname(file), spec); if (!target) continue; if (!fs.existsSync(target)) { if (fs.existsSync(`${target}.js`)) target = `${target}.js`; else if (fs.existsSync(path.join(target, 'index.js'))) target = path.join(target, 'index.js'); else continue; } const names = exportedNames(target); if (names.has('*')) continue; for (const part of m[1].split(',')) { const seg = part.trim(); if (!seg) continue; const name = seg.split(/\s+as\s+/)[0].trim(); if (!names.has(name)) { problems.push(`${path.relative(root, file)} -> ${spec} 缺少导出 ${name}`); } } } } expect(problems).toEqual([]); }); });