fix(miniapp): 导出 normalizeUserId 修复入驻页编译失败
- api.js 的 normalizeUserId 之前是内部函数,apply.vue 具名导入导致 HBuilderX 编译报错,现改为 export - 新增 miniapp-imports.spec.ts:静态校验小程序所有具名导入都存在对应导出,避免再出现只能靠人工编译才发现的问题(108 套件/711 用例全绿)
这个提交包含在:
@@ -554,9 +554,9 @@ export async function viewPost(id) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化用户ID,自动兼容历史测试账号映射 (将虚拟陪伴师 101 映射至真实用户 2006)
|
||||
* 规范化用户ID:登录态以服务端 token 为准(本地缺失时返回 0)
|
||||
*/
|
||||
function normalizeUserId(uid) {
|
||||
export function normalizeUserId(uid) {
|
||||
let target = uid;
|
||||
if (!target && target !== 0) {
|
||||
try {
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户