rollup-插件钩子及使用场景
rollup
作为一个轻量、良好的插件系统专注于esmodule
的打包器,对于插件开发提供了很多友好的钩子方法特此记录并根据遇到的问题解决办法持续更新.
范式约定
插件编写规范
- 插件名称以
rollup-plugin-
作为前缀 - 在
package.json
中包含rollup-plugin
关键字 - 建议单元测试并支持
Promise
开箱即用 - 开发中尽可能使用异步方法,例如
fs.readFile
而不是fs.readFileSync
- 使用英文作为文档语言
- 尽可能输出正确的
source-map
- 使用
virtual-module
时插件名称尽可能使用\0
作为前缀,避免与其他插件冲突
插件钩子种类
async
:此钩子将可能返回一个Promise
resolve后的同类型结果,否则会被标记为同步钩子first
:如果多个插件调用此钩子,在顺序运行的情况下当返回不为null
或undefined
时停止,否则就会交由下一个插件处理sequential
:如果多个插件调用此钩子,会根据指定顺序运行,如果其中一个为异步钩子时其他钩子会等待它resolved
后再运行parallel
:运行方式与sequential
相似,只不过在顺序钩子中执行时不会等待异步钩子resolved
,各自并行运行
插件钩子形式
除了作为一个函数外,也可以是一个包含order
、sequential
、handler
属性的对象,sequential
只能用于设置parallel
类型的钩子是否等待异步钩子执行后顺序执行,具体说明参考sequential specification
export default function example() { // return function(source) { // // ... // } return { name: 'resolve-first', resolveId: { order: 'pre', // 'pre' | 'post' | null用以指定钩子在相同钩子里的执行顺序 handler(source) { // 处理函数 // ... } }, // 当需要在相互依赖的不同 writeBundle 钩子中运行多个命令行工具的情形,需要手动设置 writeBundle: { sequential: true, order: 'post', async handler({ dir }) { const topLevelFiles = await readdir(resolve(dir)); console.log(topLevelFiles); } } }}
构建阶段钩子
构建阶段钩子按照顺序执行为:
options
用于修改或操控配置在未被rollup.rollup()
接收执行之前触发,读取配置的场景更推荐使用buildStart
因为可能存在多个options
插件对配置进行了修改
buildStart
当rollup.rollup()
被调用时触发,此时所有插件的options
都已经处理并转换过可以在此钩子访问最终的配置信息(包含默认配置)
resolveId
当模块被引用解析时被调用,一般用做对引入模块的信息进行修改处理并在load
钩子中用作自定义处理
load
自定义加载器,可以返回指定代码片段或者{ code, ast, map }
对象,以官方virtual
插件为例,引入对应虚拟模块返回定义的代码:
export default { input: 'entry', plugins: [ virtual({ entry: ` console.log('this is virtual entry content') ` }) ]}
import * as path from 'path';import type { Plugin } from 'rollup';import type { RollupVirtualOptions } from '../';
const PREFIX = `\0virtual:`;
export default function virtual(modules: RollupVirtualOptions): Plugin { const resolvedIds = new Map<string, string>();
// 遍历传入的虚拟模块对象并保存在map中 Object.keys(modules).forEach((id) => { resolvedIds.set(path.resolve(id), modules[id]); });
return { name: 'virtual', resolveId(id, importer) { // 如果解析的模块存在于传入的对象中,则增加自定义前缀以供 // load逻辑判断处理 if (id in modules) return PREFIX + id;
if (importer) { // 获取实际模块解析路径 const importerNoPrefix = importer.startsWith(PREFIX) ? importer.slice(PREFIX.length) : importer; const resolved = path.resolve(path.dirname(importerNoPrefix), id); // 如果被解析模块存在缓存map中则返回前缀拼接形式的 if (resolvedIds.has(resolved)) return PREFIX + resolved; } // 否则交由其他resolveId钩子处理 return null; }, load(id) { // 只对拥有自定义前缀的模块返回对应模块时分别在传入对象和缓存中试图返回对应代码 if (id.startsWith(PREFIX)) { const idNoPrefix = id.slice(PREFIX.length);
return idNoPrefix in modules ? modules[idNoPrefix] : resolvedIds.get(idNoPrefix); } return null; } };}
shouldTransformCachedModule
如果load
钩子加载的代码与缓存副本中的相同则跳过transform
钩子使用模块缓存并交由moduleParsed
钩子,如果shouldTransformCachedModule
返回true
则从缓存中删除此模块并重新执行transform
transform
常用于对单独模块通过ast
对code
进行转换操作并返回{ ast, map, ast }
对象对源代码修改、返回null
交由其他插件钩子处理,以官方strip
插件为例对源码进行debugger
语句方法消除:
import { walk } from 'estree-walker';import MagicString from 'magic-string';import { createFilter } from '@rollup/pluginutils';
const whitespace = /\s/;
function getName(node) { if (node.type === 'Identifier') return node.name; if (node.type === 'ThisExpression') return 'this'; if (node.type === 'Super') return 'super'; return null;}
function flatten(node) { const parts = []; while (node.type === 'MemberExpression') { if (node.computed) return null; parts.unshift(node.property.name); node = node.object; } const name = getName(node); if (!name) return null; parts.unshift(name); return parts.join('.');}
export default function strip(options = {}) { const include = options.include || '**/*.js'; const { exclude } = options; const filter = createFilter(include, exclude); const sourceMap = options.sourceMap !== false;
const removeDebuggerStatements = options.debugger !== false; const functions = (options.functions || ['console.*', 'assert.*']).map((keypath) => keypath.replace(/\*/g, '\\w+').replace(/\./g, '\\s*\\.\\s*') );
const labels = options.labels || [];
const labelsPatterns = labels.map((l) => `${l}\\s*:`);
const firstPass = [...functions, ...labelsPatterns]; if (removeDebuggerStatements) { firstPass.push('debugger\\b'); }
const reFunctions = new RegExp(`^(?:${functions.join('|')})$`); const reFirstpass = new RegExp(`\\b(?:${firstPass.join('|')})`); const firstPassFilter = firstPass.length > 0 ? (code) => reFirstpass.test(code) : () => false; const UNCHANGED = null;
return { name: 'strip', transform(code, id) { if (!filter(id) || !firstPassFilter(code)) { return UNCHANGED; }
let ast;
try { // 使用rollup上下文方法通过acron解析语法树 ast = this.parse(code); } catch (err) { err.message += ` in ${id}`; throw err; }
let edited = false; const magicString = new MagicString(code);
// 删除指定语法树节点 function remove(start, end) { while (whitespace.test(code[start - 1])) start -= 1; magicString.remove(start, end); }
// 是否块声明语句 function isBlock(node) { return node && (node.type === 'BlockStatement' || node.type === 'Program'); }
// 删除表达式 function removeExpression(node) { const { parent } = node;
if (parent.type === 'ExpressionStatement') { removeStatement(parent); } else { magicString.overwrite(node.start, node.end, '(void 0)'); }
edited = true; }
// 删除声明语句 function removeStatement(node) { const { parent } = node;
if (isBlock(parent)) { remove(node.start, node.end); } else { magicString.overwrite(node.start, node.end, '(void 0);'); }
edited = true; }
walk(ast, { enter(node, parent) { Object.defineProperty(node, 'parent', { value: parent, enumerable: false, configurable: true });
if (sourceMap) { magicString.addSourcemapLocation(node.start); magicString.addSourcemapLocation(node.end); }
// 删除debugger语句 if (removeDebuggerStatements && node.type === 'DebuggerStatement') { removeStatement(node); this.skip(); } // 删除用户自定义配置label语句 else if (node.type === 'LabeledStatement') { if (node.label && labels.includes(node.label.name)) { removeStatement(node); this.skip(); } } // 删除调用表达式 else if (node.type === 'CallExpression') { const keypath = flatten(node.callee); if (keypath && reFunctions.test(keypath)) { removeExpression(node); this.skip(); } } } });
if (!edited) return UNCHANGED;
code = magicString.toString(); const map = sourceMap ? magicString.generateMap() : null;
// 将最终转换修改后的代码返回 return { code, map }; } };}
moduleParsed
当每个模块被完全解析后调用,传入的参数为this.getModuleInfo
返回值对于一些动态加载模块和元信息并不完成,如果需要完整的模块信息可在buildEnd
中获取
resolveDynamicImport
每当动态加载模块时被调用,当所有plugin钩子返回null
时保持原样作为external
处理,返回ResolvedId
形式对象{ id, external }
会被resolveId
钩子处理,返回string
时并会作为id
被load
钩子处理
export default { input: "src/index.js", output: { dir: './dist', format: 'esm', name: 'myBundle' }, plugins: [ { load(id) { console.log('load>>>>>>>>>>>>>', id) }, resolveId(id) { console.log('resolveId>>>>>>>>>>>', id) }, async resolveDynamicImport(specifier, importer) { return 'src/someB.js' // return await this.resolve('./src/someB.js') } } ]}
buildEnd
generate
或write
调用前,所有构建打包后执行
生成产出阶段钩子
生成产出阶段钩子按照顺序执行为:
outputOptions
此钩子对构建配置选项进行替换修改操作,返回null
什么都不做.推荐使用renderStart
来访问所有此类钩子处理后的配置
renderStart
在bundle.generate()
或bundle.write()
被调用时调用,一般用于获取outputOptions
同样可以接收传递给bundle()
的inputOptions
renderDynamicImport
此钩子提供了对动态引入import('xxx.js')
的左位和右位进行细粒度自定义替换,返回null
时交由其他钩子处理最终返回指定格式默认值
function dynamicImportPolyfillPlugin() { return { name: 'dynamic-import-polyfill', renderDynamicImport() { return { left: 'dynamicImportPolyfill(', right: ', import.meta.url)' }; } };}
// 对于动态引入的语句会被替换成如下// import('test.js') -> dynamicImportPolyfill('test.js', import.meta.url)