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)) {