Open banli17 opened 2 years ago
为了影响构建过程,插件对象需要包含钩子,通过钩子,钩子函数在构建的一系列阶段调用。甚至可以修改完整的构建过程。
构建钩子有4类:
async: 异步钩子,返回一个 promise,否则会标记为 sync 钩子
first: 多个插件实现这个钩子时,钩子会顺序执行。直到某个钩子返回非 null/undefined 值就结束。相当于 webpack bail 钩子。
sequential: 多个插件实现了这个钩子时,钩子按顺序执行,如果钩子是 async,会等前一个钩子执行完才执行下一个。
parallel: 多个插件实现了这个钩子时,钩子并行执行。
钩子可以是函数,也可以是对象,如果是对象,需要指定属性 handler
作为处理函数。还有额外属性:
export default function resolveFirst() {
return {
name: 'resolve-first',
resolveId: {
order: 'pre',
handler(source) {
if (source === 'external') {
return { id: source, external: true };
}
return null;
}
}
};
}
在 writeBundle 时执行某任务,且依赖其它文件时,可能很有用(如果可能,建议在 generateBundle 钩子中添加/删除文件,速度更快,适用于纯内存构建)。
import { resolve } from 'node:path';
import { readdir } from 'node:fs/promises';
export default function getFilesOnDisk() {
return {
name: 'getFilesOnDisk',
writeBundle: {
sequential: true,
order: 'post',
async handler({ dir }) {
const topLevelFiles = await readdir(resolve(dir));
console.log(topLevelFiles);
}
}
};
}
构建阶段,其实是执行了 rollup.rollup(inputOptions) 。主要是处理 输入文件定位、加载和转换输入文件。构建阶段的第一个钩子是 options,最后一个总是 buildEnd. 如果有构建错误,closeBundle将在此之后调用。
在 watch 模式下,watchChange 在需要重新构建时触发。当观察者关闭时,closeWatcher 将触发。
buildEnd 类型: (error?: Error) => void 种类: async, parallel 上一个钩子: moduleParsed,resolveId或resolveDynamicImport。 Next Hook: outputOptions在输出生成阶段,因为这是构建阶段的最后一个钩子。
在 rollup 完成打包时调用,但在 generate 和 write 之前;可以返回一个 Promise。如果在构建过程中发生错误,它将传递给此钩子。
buildStart Type: (options: InputOptions) => void Kind: async, parallel Previous Hook: options Next Hook: resolveId
每次执行 rollup.rollup()
时调用. 如果要获取 options,推荐使用在这个钩子获取,它可以拿到完整经过转换的 options。因为 options 阶段可以修改选项。
closeWatcher Type: () => void Kind: async, parallel Previous/Next Hook: 构建和输出阶段都会触发,触发后,当前构建将继续,但是 watchChange 不会再触发新的 event 事件。
当观察者进程关闭时触发,以便所有打开的资源也可以关闭。如果返回 Promise,Rollup 将等待 Promise resolve,然后再关闭进程。这个钩子不能用于输出文件。
load Type: (id: string) => string | null | {code: string, map?: string | SourceMap, ast? : ESTree.Program, moduleSideEffects?: boolean | "no-treeshake" | null, syntheticNamedExports?: boolean | string | null, meta?: {[plugin: string]: any} | null} Kind: async, first Previous Hook: resolveId 或 resolveDynamicImport 另外,这个钩子会在调用 this.load 时触发. Next Hook: transform 转换 loaded 的文件。 shouldTransformCachedModule.
可以在这里自定义 loader 加载器,返回 null 会走默认的 load 函数(从系统文件加载文件)。为了防止解析开销,这个钩子会使用 this.parse 解析代码成 ast,这个钩子也可以返回iu一个 { code, ast, map } 对象. ast 必须是标准的 ESTree ast,每个 node 都有 start, end 属性,没有没有修改代码,可以将 map 设置为 null。否则需要自己生成map。查看 the section on source code transformations.
moduleSideEffects 如果返回值 是
null 或省略 , moduleSideEffects 将会取决于 第一个 resolveId 钩子解析的 treeshake.moduleSideEffects 选项值, 否则默认值为 true. transform 钩子可以改写它。
synthetic named exports 如果是 null 或省略,syntheticNamedExports 将会取决于 第一个 resolveId 钩子解析模块, 否则默认值为 false. transform 钩子可以改写它。
custom module meta-data 展示了如何使用 meta 选项. 如果返回了 meta 对象, 它会和 resolveId 里返回的 meta 对象合并,如果都没有返回 meta 对象,它会是空对象 ,transform hook可以 修改 meta 对象.
this.getModuleInfo 可以获取 moduleSideEffects, syntheticNamedExports 和 meta 先前的值。
moduleParsed Type: (moduleInfo: ModuleInfo) => void Kind: async, parallel Previous Hook: transform where the currently handled file was transformed. Next Hook: resolveId and resolveDynamicImport to resolve all discovered static and dynamic imports in parallel if present, otherwise buildEnd.
This hook is called each time a module has been fully parsed by Rollup. See this.getModuleInfo for what information is passed to this hook.
In contrast to the transform hook, this hook is never cached and can be used to get information about both cached and other modules, including the final shape of the meta property, the code and the ast.
This hook will wait until all imports are resolved so that the information in moduleInfo.importedIds, moduleInfo.dynamicallyImportedIds, moduleInfo.importedIdResolutions, and moduleInfo.dynamicallyImportedIdResolutions is complete and accurate. Note however that information about importing modules may be incomplete as additional importers could be discovered later. If you need this information, use the buildEnd hook.
options Type: (options: InputOptions) => InputOptions | null Kind: async, sequential Previous Hook: This is the first hook of the build phase. Next Hook: buildStart
Replaces or manipulates the options object passed to rollup.rollup. Returning null does not replace anything. If you just need to read the options, it is recommended to use the buildStart hook as that hook has access to the options after the transformations from all options hooks have been taken into account.
This is the only hook that does not have access to most plugin context utility functions as it is run before rollup is fully configured.
resolveDynamicImport Type: (specifier: string | ESTree.Node, importer: string) => string | false | null | {id: string, external?: boolean} Kind: async, first Previous Hook: moduleParsed for the importing file. Next Hook: load if the hook resolved with an id that has not yet been loaded, resolveId if the dynamic import contains a string and was not resolved by the hook, otherwise buildEnd.
Defines a custom resolver for dynamic imports. Returning false signals that the import should be kept as it is and not be passed to other resolvers thus making it external. Similar to the resolveId hook, you can also return an object to resolve the import to a different id while marking it as external at the same time.
In case a dynamic import is passed a string as argument, a string returned from this hook will be interpreted as an existing module id while returning null will defer to other resolvers and eventually to resolveId .
In case a dynamic import is not passed a string as argument, this hook gets access to the raw AST nodes to analyze and behaves slightly different in the following ways:
If all plugins return null, the import is treated as external without a warning. If a string is returned, this string is not interpreted as a module id but is instead used as a replacement for the import argument. It is the responsibility of the plugin to make sure the generated code is valid. To resolve such an import to an existing module, you can still return an object {id, external}. Note that the return value of this hook will not be passed to resolveId afterwards; if you need access to the static resolution algorithm, you can use this.resolve(source, importer) on the plugin context.
resolveId Type: (source: string, importer: string | undefined, options: {isEntry: boolean, custom?: {[plugin: string]: any}) => string | false | null | {id: string, external?: boolean | "relative" | "absolute", moduleSideEffects?: boolean | "no-treeshake" | null, syntheticNamedExports?: boolean | string | null, meta?: {[plugin: string]: any} | null} Kind: async, first Previous Hook: buildStart if we are resolving an entry point, moduleParsed if we are resolving an import, or as fallback for resolveDynamicImport. Additionally, this hook can be triggered during the build phase from plugin hooks by calling this.emitFile to emit an entry point or at any time by calling this.resolve to manually resolve an id. Next Hook: load if the resolved id that has not yet been loaded, otherwise buildEnd.
自定义resolver , 定位依赖时很有用。
import { foo } from '../bar.js';
文件定位到 "../bar.js",importer 参数是完整的解析 id,解析入口文件时,importer 通常时 undefined。一个例外是,通过 this.emitFile 生成入口文件, 你可以提供一个 importer 参数.
isEntry 属性表示 是否是 entry point, 或 emitted chunk, 它可以传给 this.resolve 上下文.
// We prefix the polyfill id with \0 to tell other plugins not to try to load or
// transform it
const POLYFILL_ID = '\0polyfill';
const PROXY_SUFFIX = '?inject-polyfill-proxy';
function injectPolyfillPlugin() {
return {
name: 'inject-polyfill',
async resolveId(source, importer, options) {
if (source === POLYFILL_ID) {
// It is important that side effects are always respected for polyfills,
// otherwise using "treeshake.moduleSideEffects: false" may prevent the
// polyfill from being included.
return { id: POLYFILL_ID, moduleSideEffects: true };
}
if (options.isEntry) {
// Determine what the actual entry would have been. We need "skipSelf"
// to avoid an infinite loop.
const resolution = await this.resolve(source, importer, { skipSelf: true, ...options });
// If it cannot be resolved or is external, just return it so that
// Rollup can display an error
if (!resolution || resolution.external) return resolution;
// In the load hook of the proxy, we need to know if the entry has a
// default export. There, however, we no longer have the full
// "resolution" object that may contain meta-data from other plugins
// that is only added on first load. Therefore we trigger loading here.
const moduleInfo = await this.load(resolution);
// We need to make sure side effects in the original entry point
// are respected even for treeshake.moduleSideEffects: false.
// "moduleSideEffects" is a writable property on ModuleInfo.
moduleInfo.moduleSideEffects = true;
// It is important that the new entry does not start with \0 and
// has the same directory as the original one to not mess up
// relative external import generation. Also keeping the name and
// just adding a "?query" to the end ensures that preserveModules
// will generate the original entry name for this entry.
return `${resolution.id}${PROXY_SUFFIX}`;
}
return null;
},
load(id) {
if (id === POLYFILL_ID) {
// Replace with actual polyfill
return "console.log('polyfill');";
}
if (id.endsWith(PROXY_SUFFIX)) {
const entryId = id.slice(0, -PROXY_SUFFIX.length);
// We know ModuleInfo.hasDefaultExport is reliable because we awaited
// this.load in resolveId
const { hasDefaultExport } = this.getModuleInfo(entryId);
let code =
`import ${JSON.stringify(POLYFILL_ID)};` + `export * from ${JSON.stringify(entryId)};`;
// Namespace reexports do not reexport default, so we need special
// handling here
if (hasDefaultExport) {
code += `export { default } from ${JSON.stringify(entryId)};`;
}
return code;
}
return null;
}
};
}
https://rollupjs.org/guide/en/#build-hooks