描述将 grunt 插件迁移到 ESM 并保留 CommonJS 兼容
解读
面试官想知道你是否理解 Grunt 插件生态仍以 CommonJS 为主 的现实,同时能否在 国内公司渐进式迁移到 ESM 的大趋势下,给出一条“不破坏老项目、又能跑新规范”的落地路线。核心考点有三:
- 如何把插件源码本身写成 ESM;
- 如何通过条件导出让旧 Gruntfile(require)无感调用;
- 如何在 CI、私有 npm、企业防火墙等国内常见限制下保证双轨产物可安装、可调试、可回滚。
知识点
- package.json 的 type 字段与条件导出(exports)
- 双构建脚本:tsc / babel / rollup 同时输出 ESM 与 CJS
- __dirname、__filename 在 ESM 中的替代方案(import.meta.url)
- Grunt 的 task 注册机制:grunt.registerMultiTask 必须在运行时能被 require 到
- 国内镜像源(npmmirror、tnpm)对“exports”字段的缓存延迟问题
- Node 版本策略:企业内常锁 14.x,需降级使用 cjs-module-lexer 兼容
- 灰度方案:peerDependenciesMeta + optionalRequire 做软降级
答案
-
源码改造
把任务逻辑全部迁到src/index.mjs,用 ESM 语法书写;对__dirname统一改写为fileURLToPath(new URL('.', import.meta.url)),避免在 ESM 下报错。 -
双轨构建
在package.json中保留"type": "module",同时用 rollup + @rollup/plugin-commonjs + @rollup/plugin-babel 打出两条产物:dist/index.mjs(原生 ESM)dist/index.cjs(CommonJS,带module.exports =)
并在"scripts"里写"prepublishOnly": "npm run build:cjs && npm run build:esm",保证发布前产物最新。
-
条件导出
新增 exports 字段做路由:"exports": { ".": { "import": "./dist/index.mjs", "require": "./dist/index.cjs", "default": "./dist/index.cjs" } }这样老项目
require('grunt-xxx')会命中 cjs,新项目import xxx from 'grunt-xxx'会命中 esm,零配置切换。 -
Grunt 插件入口兼容
由于 Grunt 通过require.resolve加载插件,必须在 cjs 入口里再包一层 grunt.task 注册:// dist/index.cjs const plugin = require('./plugin.cjs'); // 真正的 task 实现 module.exports = function(grunt) { grunt.registerMultiTask('xxx', plugin.description, plugin.run); };保证
grunt.loadNpmTasks('grunt-xxx')依旧可用。 -
国内落地细节
- 私有 npm 仓库(verdaccio/nexus)需 手动清空缓存才能识别新的 exports 字段;
- 若企业锁 Node 14,避免使用顶层 await,防止 14.13 以下解析失败;
- 提供 fallback 入口:在
main字段仍指向dist/index.cjs,防止老版本 npm(<7)无法识别 exports 时直接崩溃; - 在 README 中写明 “若安装后 grunt 无法识别任务,请删除 node_modules/.cache 与 package-lock”,减少运维沟通成本。
-
验证流程
在 example/ 目录放两份 Gruntfile:Gruntfile.js使用requireGruntfile.mjs使用import
在 CI 里跑grunt xxx --gruntfile Gruntfile.js与grunt xxx --gruntfile Gruntfile.mjs,双轨任务必须同时通过,才允许合并主干。
拓展思考
- 渐进式废弃 CommonJS:在下一个大版本把
"main"指向 ESM,exports 里去掉 require,但提前一个 minor 版本给出 deprecation 日志,让国内存量项目有半年缓冲。 - 微前端场景:如果插件还要被 vite/rollup 原生调用,可再导出
dist/index.browser.mjs,用 条件导出中的 browser 字段做环境隔离,避免 Node-only 的 fs 模块被打包进去。 - monorepo 下的调试效率:用 pnpm workspace overrides 把本地插件链接到 example 项目,配合
nodemon --watch src --exec "npm run build",保存即刷新,解决“双构建慢”的痛点。