描述在 monorepo 中按包拆分覆盖率指标

解读

国内前端团队普遍采用 monorepo 管理业务组件库、微应用或 BFF 层,覆盖率指标必须拆到“包”维度才能精准定位问题、避免“大锅饭”式数据。面试官想知道:

  1. 你是否理解 monorepo 下“单测分散、报告聚合”的痛点;
  2. 能否用 Grunt 生态把 nyc / c8 / istanbul 的原始报告切成“每个 npm package 一份”,并生成可落地的门禁(gate)规则;
  3. 是否熟悉国内 CI(GitLab-CI、Jenkins、云效)对“包级覆盖率上报”的规范,以及如何把 Grunt 任务无缝接入。

知识点

  • Grunt 多任务模式:grunt.registerMultiTask 可动态为每个子包生成独立任务,避免手写重复配置。
  • istanbul/nyc API:通过 nyc report --reporter=json --report-dir=./coverage/pkg-a 生成 按包隔离的 json 摘要,再使用 nyc mergeistanbul-combine 做二次聚合。
  • lerna/@nx/devkit 包元数据:读取 lerna.json / nx.json / package.json workspaces 字段,运行时枚举包路径,保证新增包无需改 Gruntfile。
  • 自定义 Grunt 插件规范:官方模板 grunt-init-gruntplugin 要求任务文件导出一个 function(grunt),通过 grunt.file.expand 遍历 packages/*/test 目录,动态注入 nyc exec mocha 命令。
  • 国内门禁实践:阿里、字节内部要求“增量行覆盖率 ≥ 80 % 且包级覆盖率不下降”,Grunt 任务需在本地 grunt coverage 阶段生成 coverage-summary.pkg-a.json,CI 侧通过 @alife/coverage-gate 包做 PR 评论。
  • Source-map 对齐:monorepo 常共用顶层 tsconfig.build.json,必须给每个包单独生成 source-map,否则 istanbul 会出现“路径漂移”导致行号错位。
  • LCOV 上传工具:国内常用 Codecov 企业版、阿里云效 CodeCoverage、腾讯云 CODING 覆盖率服务,它们都要求子包报告带 prefix=packages/pkg-a 字段,Grunt 任务里用 sed -i 's/^SF:/SF:packages\/pkg-a\//' *.lcov 修正路径。

答案

  1. 在顶层安装依赖

    npm i -D grunt grunt-contrib-clean nyc mocha istanbul-merge-json
    
  2. 编写 tasks/coverage-per-pkg.js 自定义任务

    module.exports = function(grunt){
      grunt.registerMultiTask('coveragePkg', '按包跑单测并生成独立覆盖率', function(){
        const done  = this.async();
        const pkg   = this.data.pkg;          // 子包绝对路径
        const name  = require(pkg+'/package.json').name;
        const cmd   = `nyc --reporter=json --report-dir=../../coverage/${name} ` +
                      `--cwd ${pkg} mocha 'test/**/*.test.js'`;
        grunt.util.spawn({cmd:'bash', args:['-c',cmd]}, (e,r,code)=>{
          if(code!==0) grunt.fail.warn(`${name} 单测失败`);
          // 生成摘要供门禁使用
          const summary = grunt.file.readJSON(`coverage/${name}/coverage-final.json`);
          const total   = summary.total;
          grunt.file.write(`coverage/${name}/coverage-summary.json`, JSON.stringify({
            name, lines:total.lines.pct, statements:total.statements.pct,
            functions:total.functions.pct, branches:total.branches.pct
          },null,2));
          done();
        });
      });
    };
    
  3. 在 Gruntfile.js 中动态注册子任务

    module.exports = function(grunt){
      const {getPackages} = require('@lerna/project'); // 或 nx
      grunt.initConfig({
        clean:{ coverage:['coverage'] }
      });
      getPackages().then(pkgs=>{
        const cfg = {};
        pkgs.forEach(p=>{
          cfg[p.name] = {pkg:p.location};
        });
        grunt.config('coveragePkg', cfg);
        grunt.registerTask('coverage',
          ['clean:coverage', ...Object.keys(cfg).map(n=>`coveragePkg:${n}`)]
        );
      });
    };
    
  4. 运行与验证

    grunt coverage
    

    产出

    coverage/
      ├─ @foo/button/coverage-summary.json
      ├─ @foo/table/coverage-summary.json
      └─ lcov.info   (合并后给 GitLab 上传)
    

    每个 summary 文件可直接被 云效覆盖率门禁 读取,实现“包级不达标,MR 无法合并”。

拓展思考

  • 增量覆盖率:在 coveragePkg 任务里追加 --changed-files=origin/main 参数,只跑有变动的包,10 万行级 monorepo 本地 30 秒完成。
  • 可视化报表:把各包 summary 合并成 coverage/coverage-matrix.json,用 自研低代码平台 渲染成“包-指标”热力图,方便架构委员会做技术债排期。
  • 与 Vite 共存:若子包已迁移到 Vitest,可在 Grunt 任务里用 vitest run --coverage 替代 nyc,通过 c8 --reporter=lcov 保持同样出口格式,实现老 Grunt 流水线对新测试框架的透明兼容。