如何批量检测过期插件并自动生成升级报告

解读

在国内真实的前端工程里,Grunt 项目往往“年久失修”,package.json 里插件版本号仍停留在 0.x 或 1.x,npm audit 只能扫安全漏洞,无法识别“功能已过时”的插件;而手动比对 4000+ 插件的更新日志又极不现实。面试官想考察的是:

  1. 能否用工程化手段一次性发现所有过期插件(含主版本落后、官方弃用、仓库归档);
  2. 能否自动生成中文升级报告(含风险分级、Breaking Change 摘要、国内镜像下载耗时预估),供测试、运维、产品多方评审;
  3. 脚本必须可集成到现有 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 自动触发,报告上传至企业微信机器人,@前端负责人。

答案

  1. 安装依赖 npm i -D semver axios moment lodash grunt-contrib-handlebars

  2. 新建任务文件 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(); }); }); };

  3. 模板片段(grunt/templates/upgrade-report.hbs)

<html lang="zh-CN"><head><meta charset="utf-8"><title>Grunt 插件升级报告</title></head> <body><h2>过期插件列表({{date}})</h2> <ul>{{#each list}} <li><b>{{name}}</b> 当前{{current}} → 最新{{latest}} {{#if deprecated}}<span style="color:red">【官方弃用】</span>{{/if}}</li> {{/each}}</ul></body></html> 4. 注册默认任务 grunt.registerTask('default', ['upgrade-report']); 5. 运行 grunt upgrade-report 即可在 reports 目录得到带时间戳的 html 报告,**仅列出本次新增的过期插件**,可直接发企业微信或邮件。

拓展思考

  • 如何识别“间接依赖”过期:使用 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 阶段自动跳过,减少噪音。