From f1b3cc85e245864c2e37eae1e7b78ec87a634ed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E9=93=B6=E5=86=9B?= Date: Sat, 19 Sep 2026 23:46:21 +0800 Subject: [PATCH] =?UTF-8?q?fix(scan):=20=E5=8C=85=E6=8F=8F=E8=BF=B0/?= =?UTF-8?q?=E4=B8=BB=E9=A1=B5/=E4=BB=93=E5=BA=93/=E4=BD=9C=E8=80=85/?= =?UTF-8?q?=E8=AE=B8=E5=8F=AF=E8=AF=BB=E5=8F=96=E4=B8=8D=E5=88=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verdaccio 落盘的元数据顶层不含 description/homepage 等字段(实测 只有 name/versions/time/dist-tags/readme),但这些字段完整存在于 每个 versions[v] 里。新增 metaField 回退逻辑:顶层取不到时按 semver 排序取最新版本里的字段,修复检索页/详情抽屉描述全为「—」。 --- backend/scanner.js | 55 +++++++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/backend/scanner.js b/backend/scanner.js index 8ed17f5..4d315dc 100644 --- a/backend/scanner.js +++ b/backend/scanner.js @@ -109,6 +109,31 @@ function latestDate(metaUpdated, tgzMtime) { return [metaUpdated, tgzUpdated].filter(Boolean).sort().pop() || null } +/** + * 包信息字段提取(description/homepage/repository/author/license) + * Verdaccio 落盘的元数据顶层不含这些字段(实测只有 name/versions/time/dist-tags/readme 等), + * 但每个 versions[v] 里都有完整字段。因此先读顶层,取不到时按 semver 数值排序回退到最新版本。 + * @param {object} meta 落盘的 package.json 元数据 + * @param {object} versionsObj meta.versions + * @param {string} key 字段名 + * @param {(src:any)=>string} [picker] 字段为对象时(repository.url / author.name)取值 + */ +function metaField(meta, versionsObj, key, picker) { + const pick = src => { + if (!src) return null + const v = typeof src === 'string' ? src : (picker ? picker(src) : null) + return typeof v === 'string' && v ? v : null + } + const top = pick(meta[key]) + if (top) return top + const sorted = Object.entries(versionsObj).sort((a, b) => a[0].localeCompare(b[0], undefined, { numeric: true })) + for (let i = sorted.length - 1; i >= 0; i--) { + const v = pick(sorted[i][1] && sorted[i][1][key]) + if (v) return v + } + return null +} + /** * 扫描 storage 目录 * @returns {Array} 包列表(versionsDetail: [{version, size(MB), date}]) @@ -163,15 +188,12 @@ export function scanStorage(storageDir) { const timeObj = meta.time || {} const distTags = meta['dist-tags'] || {} // 包信息(来自本地元数据,离线可用):介绍/主页/仓库/作者/许可 - const description = typeof meta.description === 'string' ? meta.description : null - const homepage = typeof meta.homepage === 'string' && meta.homepage ? meta.homepage : null - const repository = typeof meta.repository === 'string' - ? meta.repository - : (meta.repository && typeof meta.repository.url === 'string' ? meta.repository.url : null) - const author = typeof meta.author === 'string' - ? meta.author - : (meta.author && typeof meta.author.name === 'string' ? meta.author.name : null) - const license = typeof meta.license === 'string' ? meta.license : null + // 顶层缺失时回退到最新版本里的字段(Verdaccio 落盘元数据顶层不含这些) + const description = metaField(meta, versionsObj, 'description') + const homepage = metaField(meta, versionsObj, 'homepage') + const repository = metaField(meta, versionsObj, 'repository', r => r.url) + const author = metaField(meta, versionsObj, 'author', a => a.name) + const license = metaField(meta, versionsObj, 'license') const versionsDetail = [] // 总大小以实际 .tgz 文件为准 @@ -279,15 +301,12 @@ export async function scanStorageAsync(storageDir, onProgress) { const timeObj = meta.time || {} const distTags = meta['dist-tags'] || {} // 包信息(来自本地元数据,离线可用):介绍/主页/仓库/作者/许可 - const description = typeof meta.description === 'string' ? meta.description : null - const homepage = typeof meta.homepage === 'string' && meta.homepage ? meta.homepage : null - const repository = typeof meta.repository === 'string' - ? meta.repository - : (meta.repository && typeof meta.repository.url === 'string' ? meta.repository.url : null) - const author = typeof meta.author === 'string' - ? meta.author - : (meta.author && typeof meta.author.name === 'string' ? meta.author.name : null) - const license = typeof meta.license === 'string' ? meta.license : null + // 顶层缺失时回退到最新版本里的字段(Verdaccio 落盘元数据顶层不含这些) + const description = metaField(meta, versionsObj, 'description') + const homepage = metaField(meta, versionsObj, 'homepage') + const repository = metaField(meta, versionsObj, 'repository', r => r.url) + const author = metaField(meta, versionsObj, 'author', a => a.name) + const license = metaField(meta, versionsObj, 'license') const versionsDetail = [] const totalBytes = tgzTotal for (const [v, info] of Object.entries(versionsObj)) {