如何批量检测过期插件并自动生成升级报告
解读
在国内真实的前端工程里,Grunt 项目往往“年久失修”,package.json 里插件版本号仍停留在 0.x 或 1.x,npm audit 只能扫安全漏洞,无法识别“功能已过时”的插件;而手动比对 4000+ 插件的更新日志又极不现实。面试官想考察的是:
- 能否用工程化手段一次性发现所有过期插件(含主版本落后、官方弃用、仓库归档);
- 能否自动生成中文升级报告(含风险分级、Breaking Change 摘要、国内镜像下载耗时预估),供测试、运维、产品多方评审;
- 脚本必须可集成到现有 Grunt 流水线,零侵入老项目,且能在内网/弱网环境下运行。
知识点
- npm view / yarn info / pnpm list --json:批量获取远程最新版本、发布时间、弃用标记;
- Gruntfile 动态任务注册:grunt.registerTask('upgrade-report', function () { ... }),利用 this.async() 阻塞直至异步检测完成;
- semver 库:解析 ^ ~ * 等范围,计算主版本差;
- npmmirror 源与元数据缓存:把 registry.npm.taobao.org 的响应落盘到 .cache/grunt-upgrade/,解决国内丢包;
- 官方弃用字段检测:npm 返回的 deprecated 字段不为空即标红;
- Breaking Change 快速提取:把仓库 CHANGELOG.md 第一级 ## x.y.z 与第二级 ### BREAKING 之间的文本正则抽出,限制 240 字;
- 风险分级策略:
- P0 安全漏洞:npm audit level=high 以上;
- P1 主版本落后:major 差≥1;
- P2 半年未更新:发布时间>180 天;
- P3 官方弃用:deprecated 字段非空;
- 报告模板引擎:使用 grunt-contrib-handlebars 预编译 .hbs,输出 upgrade-report-YYYYmmdd-HHMMSS.html,内嵌折叠面板与一键复制 npm install 命令;
- 增量更新:把本次扫描结果与 reports/last.json 做 diff,仅列出“新增过期”插件,避免报告冗余;
- CI 集成:在 GitLab-CI 中配置 only: - schedules,每周一 09:00 自动触发,报告上传至企业微信机器人,@前端负责人。
答案
-
安装依赖 npm i -D semver axios moment lodash grunt-contrib-handlebars
-
新建任务文件 grunt/tasks/upgrade-report.js module.exports = function (grunt) { grunt.registerTask('upgrade-report', '批量检测过期插件并生成中文升级报告', function () { const done = this.async(); const fs = require('fs'); const path = require('path'); const semver = require('semver'); const axios = require('axios'); const moment = require('moment'); const _ = require('lodash'); const pkg = grunt.file.readJSON('package.json'); const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies); const reportFile =
reports/upgrade-report-${moment().format('YYYYMMDDHHmmss')}.html; const lastFile = 'reports/last.json'; const cacheDir = '.cache/grunt-upgrade'; if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true }); if (!fs.existsSync('reports')) fs.mkdirSync('reports');const tasks = Object.keys(deps).map(name => { const current = deps[name].replace(/[^~>=<\s]/g, ''); const cachePath = path.join(cacheDir,
${name}.json); let remote; if (fs.existsSync(cachePath)) { remote = JSON.parse(fs.readFileSync(cachePath, 'utf8')); } else { return axios.get(https://registry.npmmirror.com/${name}/latest, { timeout: 8000 }) .then(res => { remote = { version: res.data.version, time: res.data.time.modified, deprecated: res.data.deprecated }; fs.writeFileSync(cachePath, JSON.stringify(remote)); return { name, current, remote }; }) .catch(() => ({ name, current, remote: null })); } return Promise.resolve({ name, current, remote }); });Promise.all(tasks).then(results => { const list = results.filter(r => r.remote).map(r => { const latest = r.remote.version; const diff = semver.diff(r.current, latest); const risk = !diff ? 0 : r.remote.deprecated ? 3 : semver.major(latest) > semver.major(r.current) ? 1 : moment().diff(moment(r.remote.time), 'days') > 180 ? 2 : 0; return { name, current, latest, risk, deprecated: !!r.remote.deprecated }; }).filter(r => r.risk > 0);
const last = fs.existsSync(lastFile) ? JSON.parse(fs.readFileSync(lastFile)) : []; const inc = list.filter(r => !last.find(l => l.name === r.name && l.current === r.current && l.latest === r.latest));
const tpl = grunt.file.read('grunt/templates/upgrade-report.hbs'); const template = require('handlebars').compile(tpl); const html = template({ date: moment().format('YYYY年MM月DD日 HH:mm'), list: inc }); grunt.file.write(reportFile, html); grunt.file.write(lastFile, JSON.stringify(list)); grunt.log.ok(
报告已生成:${reportFile}); done(); }); }); }; -
模板片段(grunt/templates/upgrade-report.hbs)
拓展思考
- 如何识别“间接依赖”过期:使用 npm ls --json 递归遍历 node_modules 树,结合 arborist 库,把深层插件也纳入扫描,避免“lock 文件锁死”带来的隐形风险;
- 如何自动提交 PR:在 GitLab-CI 里继续调用 npm-check-updates -u,生成 commit,走 Merge Request 模板,自动 @代码 Reviewer,实现“检测-报告-升级”闭环;
- 如何兼容私有 npm 仓库:在 axios 请求头里带上 ${CI_JOB_TOKEN} 或 .npmrc 的 _auth,同时把 registry 指向内网 Verdaccio,确保内外网数据源一致;
- 如何量化升级成本:在报告里增加“webpack 5 迁移工时评估”字段,根据插件体积、Breaking Change 条数、单元测试覆盖率打分,让项目经理直接看到“人日”风险;
- 如何防止误报:维护一份 .grunt-upgrade-ignore.json,把“已知不升”的插件写死版本,CI 阶段自动跳过,减少噪音。