fix(miniapp): 导出 normalizeUserId 修复入驻页编译失败

- api.js 的 normalizeUserId 之前是内部函数,apply.vue 具名导入导致 HBuilderX 编译报错,现改为 export

- 新增 miniapp-imports.spec.ts:静态校验小程序所有具名导入都存在对应导出,避免再出现只能靠人工编译才发现的问题(108 套件/711 用例全绿)
这个提交包含在:
xcbyy
2026-10-21 11:32:03 +08:00
父节点 a249a5ee62
当前提交 9217dfcf11
共有 2 个文件被更改,包括 83 次插入 和 2 次删除

查看文件

@@ -0,0 +1,81 @@
// 已读取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([]);
});
});