From 9217dfcf115e104f43ba8931d69ef9748b637dbf Mon Sep 17 00:00:00 2001 From: xcbyy <2578898740@qq.com> Date: Wed, 21 Oct 2026 11:32:03 +0800 Subject: [PATCH] =?UTF-8?q?fix(miniapp):=20=E5=AF=BC=E5=87=BA=20normalizeU?= =?UTF-8?q?serId=20=E4=BF=AE=E5=A4=8D=E5=85=A5=E9=A9=BB=E9=A1=B5=E7=BC=96?= =?UTF-8?q?=E8=AF=91=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - api.js 的 normalizeUserId 之前是内部函数,apply.vue 具名导入导致 HBuilderX 编译报错,现改为 export - 新增 miniapp-imports.spec.ts:静态校验小程序所有具名导入都存在对应导出,避免再出现只能靠人工编译才发现的问题(108 套件/711 用例全绿) --- pwuniapp/common/api.js | 4 +- server/tests/miniapp-imports.spec.ts | 81 ++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 server/tests/miniapp-imports.spec.ts diff --git a/pwuniapp/common/api.js b/pwuniapp/common/api.js index a9e7a44..1e01b17 100644 --- a/pwuniapp/common/api.js +++ b/pwuniapp/common/api.js @@ -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 { diff --git a/server/tests/miniapp-imports.spec.ts b/server/tests/miniapp-imports.spec.ts new file mode 100644 index 0000000..cb53579 --- /dev/null +++ b/server/tests/miniapp-imports.spec.ts @@ -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 => { + 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([]); + }); +});