- api.js 的 normalizeUserId 之前是内部函数,apply.vue 具名导入导致 HBuilderX 编译报错,现改为 export - 新增 miniapp-imports.spec.ts:静态校验小程序所有具名导入都存在对应导出,避免再出现只能靠人工编译才发现的问题(108 套件/711 用例全绿)
82 行
3.1 KiB
TypeScript
82 行
3.1 KiB
TypeScript
// 已读取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<string> => {
|
||
const src = fs.readFileSync(file, 'utf-8');
|
||
const names = new Set<string>();
|
||
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([]);
|
||
});
|
||
});
|