fix(scan): 包描述/主页/仓库/作者/许可读取不到

Verdaccio 落盘的元数据顶层不含 description/homepage 等字段(实测
只有 name/versions/time/dist-tags/readme),但这些字段完整存在于
每个 versions[v] 里。新增 metaField 回退逻辑:顶层取不到时按
semver 排序取最新版本里的字段,修复检索页/详情抽屉描述全为「—」。
This commit is contained in:
陈银军
2026-09-19 23:46:21 +08:00
parent e7de52a6d1
commit f1b3cc85e2
+37 -18
View File
@@ -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)) {