插件开发
插件概述
Rollup 插件是一个对象,包含一个或多个下面描述的 属性、构建钩子 和 输出生成钩子,并遵循我们的 约定。插件应该作为包进行分发,该包导出一个函数,该函数可以使用特定于插件的选项调用,并返回这样的对象。
插件允许您通过例如在捆绑之前转译代码或在您的 node_modules
文件夹中查找第三方模块来自定义 Rollup 的行为。有关如何使用它们的示例,请参阅 使用插件。
插件列表可以在 github.com/rollup/awesome 中找到。如果您想建议一个插件,请提交一个 Pull Request。
一个简单的例子
以下插件将拦截任何对 virtual-module
的导入,而无需访问文件系统。例如,如果您想在浏览器中使用 Rollup,则需要这样做。它甚至可以用来替换入口点,如示例所示。
// rollup-plugin-my-example.js
export default function myExample () {
return {
name: 'my-example', // this name will show up in logs and errors
resolveId ( source ) {
if (source === 'virtual-module') {
// this signals that Rollup should not ask other plugins or check
// the file system to find this id
return source;
}
return null; // other ids should be handled as usually
},
load ( id ) {
if (id === 'virtual-module') {
// the source code for "virtual-module"
return 'export default "This is virtual!"';
}
return null; // other ids should be handled as usually
}
};
}
// rollup.config.js
import myExample from './rollup-plugin-my-example.js';
export default ({
input: 'virtual-module', // resolved by our plugin
plugins: [myExample()],
output: [{
file: 'bundle.js',
format: 'es'
}]
});
约定
- 插件应该有一个清晰的名称,带有
rollup-plugin-
前缀。 - 在
package.json
中包含rollup-plugin
关键字。 - 插件应该经过测试。我们推荐 mocha 或 ava,它们开箱即用地支持 Promises。
- 在可能的情况下使用异步方法,例如
fs.readFile
而不是fs.readFileSync
。 - 用英语记录您的插件。
- 确保您的插件输出正确的源映射(如果适用)。
- 如果您的插件使用“虚拟模块”(例如用于辅助函数),请在模块 ID 前添加
\0
。这将阻止其他插件尝试处理它。
属性
name
类型 | 字符串 |
---|
插件的名称,用于错误消息和日志。
version
类型 | 字符串 |
---|
插件的版本,用于插件间通信场景。
构建钩子
为了与构建过程进行交互,您的插件对象包含“钩子”。钩子是函数,在构建的不同阶段被调用。钩子可以影响构建的运行方式,提供有关构建的信息,或在构建完成后修改构建。有不同类型的钩子
async
:钩子也可以返回一个 Promise,解析为相同类型的 value;否则,钩子被标记为sync
。first
:如果多个插件实现了此钩子,则这些钩子将按顺序运行,直到一个钩子返回的值不是null
或undefined
。sequential
:如果多个插件实现了此钩子,则它们将按指定的插件顺序运行。如果钩子是async
,则此类型的后续钩子将等待当前钩子解析。parallel
:如果多个插件实现了此钩子,则它们将按指定的插件顺序运行。如果钩子是async
,则此类型的后续钩子将并行运行,不会等待当前钩子。
钩子也可以是对象,而不是函数。在这种情况下,实际的钩子函数(或 banner/footer/intro/outro
的值)必须指定为 handler
。这允许您提供更改钩子执行的附加可选属性
order: "pre" | "post" | null
如果有多个插件实现了此钩子,则要么先运行此插件("pre"
),要么最后运行("post"
),要么按用户指定的顺序运行(没有值或null
)。jsexport default function resolveFirst() { return { name: 'resolve-first', resolveId: { order: 'pre', handler(source) { if (source === 'external') { return { id: source, external: true }; } return null; } } }; }
如果多个插件使用
"pre"
或"post"
,Rollup 将按用户指定的顺序运行它们。此选项可用于所有插件钩子。对于并行钩子,它会更改同步部分运行的顺序。sequential: boolean
不要与其他插件的相同钩子并行运行此钩子。只能用于parallel
钩子。使用此选项将使 Rollup 等待所有先前插件的结果,然后执行插件钩子,然后再次并行运行剩余的插件。例如,当您有插件A
、B
、C
、D
、E
都实现了相同的并行钩子,而中间插件C
有sequential: true
时,Rollup 将首先并行运行A + B
,然后单独运行C
,然后并行运行D + E
。当您需要在不同的
writeBundle
钩子中运行多个命令行工具时,这可能很有用,这些工具相互依赖(请注意,如果可能,建议在顺序的generateBundle
钩子中添加/删除文件,尽管这更快,适用于纯内存构建,并允许其他内存构建插件看到这些文件)。您可以将此选项与order
结合使用以进行额外的排序。jsimport { 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)
触发。它们主要关注在 Rollup 处理输入文件之前定位、提供和转换输入文件。构建阶段的第一个钩子是 options
,最后一个钩子始终是 buildEnd
。如果构建过程中出现错误,则 closeBundle
将在之后被调用。
此外,在监视模式下,watchChange
钩子可以在任何时候触发,以通知在当前运行生成其输出后将触发新的运行。此外,当监视器关闭时,closeWatcher
钩子将被触发。
有关在输出生成阶段运行以修改生成的输出的钩子,请参阅 输出生成钩子。
buildEnd
类型 | (error?: Error) => void |
---|---|
种类 | async, parallel |
前一个 | moduleParsed 、resolveId 或 resolveDynamicImport |
下一个 | outputOptions 在输出生成阶段,因为这是构建阶段的最后一个钩子 |
在 Rollup 完成捆绑后调用,但在调用 generate
或 write
之前;您也可以返回一个 Promise。如果构建过程中出现错误,它将传递到此钩子。
buildStart
类型 | (options: InputOptions) => void |
---|---|
种类 | async, parallel |
前一个 | options |
下一个 | resolveId 以并行方式解析每个入口点 |
在每个 rollup.rollup
构建上调用。这是您需要访问传递给 rollup.rollup()
的选项时推荐使用的钩子,因为它考虑了所有 options
钩子的转换,并且还包含未设置选项的正确默认值。
closeWatcher
类型 | () => void |
---|---|
种类 | async, parallel |
前一个/下一个 | 此钩子可以在构建和输出生成阶段的任何时候触发。如果是这种情况,当前构建将继续进行,但不会触发任何新的 watchChange 事件 |
在监视器进程关闭时通知插件,以便所有打开的资源也可以关闭。如果返回一个 Promise,Rollup 将等待 Promise 解析,然后再关闭进程。此钩子不能由输出插件使用。
load
类型 | (id: string) => LoadResult |
---|---|
种类 | async, first |
前一个 | resolveId 或 resolveDynamicImport ,其中加载的 id 已解析。此外,此钩子可以通过调用 this.load 从插件钩子在任何时候触发,以预加载与 id 对应的模块 |
下一个 | transform 用于转换加载的文件(如果未使用缓存,或者没有使用相同 code 的缓存副本),否则 shouldTransformCachedModule |
type LoadResult = string | null | SourceDescription;
interface SourceDescription {
code: string;
map?: string | SourceMap;
ast?: ESTree.Program;
attributes?: { [key: string]: string } | null;
meta?: { [plugin: string]: any } | null;
moduleSideEffects?: boolean | 'no-treeshake' | null;
syntheticNamedExports?: boolean | string | null;
}
定义一个自定义加载器。返回 null
将委托给其他 load
函数(并最终委托给从文件系统加载的默认行为)。为了防止在例如此钩子已经使用 this.parse
生成 AST 的情况下出现额外的解析开销,此钩子可以选择返回一个 { code, ast, map }
对象。ast
必须是标准的 ESTree AST,每个节点都有 start
和 end
属性。如果转换没有移动代码,您可以通过将 map
设置为 null
来保留现有的源映射。否则,您可能需要生成源映射。请参阅有关 源代码转换 的部分。
如果 moduleSideEffects
返回 false
且没有其他模块从该模块导入任何内容,则即使该模块具有副作用,也不会将其包含在捆绑包中。如果返回 true
,Rollup 将使用其默认算法来包含模块中所有具有副作用的语句(例如修改全局变量或导出变量)。如果返回 "no-treeshake"
,则将为该模块关闭树摇,并且即使该模块为空,它也将包含在生成的块之一中。如果返回 null
或省略该标志,则 moduleSideEffects
将由解析该模块的第一个 resolveId
钩子、treeshake.moduleSideEffects
选项决定,或最终默认为 true
。transform
钩子可以覆盖此选项。
attributes
包含导入该模块时使用的导入属性。目前,它们不会影响捆绑模块的渲染,而是用于文档目的。如果返回 null
或省略该标志,则 attributes
将由解析该模块的第一个 resolveId
钩子决定,或由该模块的第一个导入中存在的属性决定。transform
钩子可以覆盖此选项。
有关 syntheticNamedExports
选项的影响,请参见 合成命名导出。如果返回 null
或省略该标志,则 syntheticNamedExports
将由解析该模块的第一个 resolveId
钩子决定,或最终默认为 false
。transform
钩子可以覆盖此选项。
有关如何使用 meta
选项,请参见 自定义模块元数据。如果此钩子返回 meta
对象,它将与 resolveId
钩子返回的任何 meta
对象浅层合并。如果没有任何钩子返回 meta
对象,它将默认为空对象。transform
钩子可以进一步添加或替换此对象的属性。
您可以使用 this.getModuleInfo
在此钩子内部找出 attributes
、meta
、moduleSideEffects
和 syntheticNamedExports
的先前值。
moduleParsed
类型 | (moduleInfo: ModuleInfo) => void |
---|---|
种类 | async, parallel |
前一个 | transform ,其中当前处理的文件已转换 |
下一个 | resolveId 和 resolveDynamicImport 用于并行解析所有发现的静态和动态导入(如果存在),否则为 buildEnd |
每次 Rollup 完全解析模块时,都会调用此钩子。有关传递给此钩子的信息,请参见 this.getModuleInfo
。
与 transform
钩子相比,此钩子从不缓存,可用于获取有关缓存模块和其他模块的信息,包括 meta
属性的最终形状、code
和 ast
。
此钩子将等待所有导入解析完成,以便 moduleInfo.importedIds
、moduleInfo.dynamicallyImportedIds
、moduleInfo.importedIdResolutions
和 moduleInfo.dynamicallyImportedIdResolutions
中的信息完整且准确。但是请注意,有关导入模块的信息可能不完整,因为以后可能会发现其他导入器。如果您需要此信息,请使用 buildEnd
钩子。
onLog
类型 | (level: LogLevel, log: RollupLog) => boolean | null |
---|---|
种类 | 同步,顺序 |
前一个/下一个 | 此钩子可以在任何时候触发。 |
有关可用的 Loglevel
值和 RollupLog
类型,请参见 onLog
选项。
一个函数,它接收并过滤 Rollup 和插件生成的日志和警告,然后再将它们传递给 onLog
选项或打印到控制台。
如果此钩子返回 false
,则将过滤日志。否则,日志将传递给下一个插件的 onLog
钩子、onLog
选项或打印到控制台。插件还可以更改日志的日志级别或将日志转换为错误,方法是将日志传递给 this.error
、this.warn
、this.info
或 this.debug
并返回 false
。请注意,与其他插件钩子(例如将插件名称添加到日志中)不同,这些函数不会添加或更改日志的属性。此外,由 onLog
钩子生成的日志不会传递回同一个插件的 onLog
钩子。如果另一个插件在其自己的 onLog
钩子中响应此类日志生成日志,则此日志也不会传递回原始 onLog
钩子。
function plugin1() {
return {
name: 'plugin1',
buildStart() {
this.info({ message: 'Hey', pluginCode: 'SPECIAL_CODE' });
},
onLog(level, log) {
if (log.plugin === 'plugin1' && log.pluginCode === 'SPECIAL_CODE') {
// We turn logs into warnings based on their code. This warnings
// will not be passed back to the same plugin to avoid an
// infinite loop, but other plugins will still receive it.
this.warn(log);
return false;
}
}
};
}
function plugin2() {
return {
name: 'plugin2',
onLog(level, log) {
if (log.plugin === 'plugin1' && log.pluginCode === 'SPECIAL_CODE') {
// You can modify logs in this hooks as well
log.meta = 'processed by plugin 2';
// This turns the log back to "info". If this happens in
// response to the first plugin, it will not be passed back to
// either plugin to avoid an infinite loop. If both plugins are
// active, the log will be an info log if the second plugin is
// placed after the first one
this.info(log);
return false;
}
}
};
}
与 options
钩子一样,此钩子无法访问大多数 插件上下文 实用程序函数,因为它可能在 Rollup 完全配置之前运行。唯一支持的属性是 this.meta
以及 this.error
、this.warn
、this.info
和 this.debug
,用于日志记录和错误。
options
类型 | (options: InputOptions) => InputOptions | null |
---|---|
种类 | 异步,顺序 |
前一个 | 这是构建阶段的第一个钩子 |
下一个 | buildStart |
替换或操作传递给 rollup.rollup
的选项对象。返回 null
不会替换任何内容。如果您只需要读取选项,建议使用 buildStart
钩子,因为该钩子在考虑了所有 options
钩子的转换后可以访问选项。
与 onLog
钩子一样,此钩子无法访问大多数 插件上下文 实用程序函数,因为它在 Rollup 完全配置之前运行。唯一支持的属性是 this.meta
以及 this.error
、this.warn
、this.info
和 this.debug
,用于日志记录和错误。
resolveDynamicImport
类型 | ResolveDynamicImportHook |
---|---|
种类 | async, first |
前一个 | moduleParsed 用于导入文件 |
下一个 | load 如果钩子解析的 id 尚未加载,则为 resolveId 如果动态导入包含字符串且未由钩子解析,否则为 buildEnd |
type ResolveDynamicImportHook = (
specifier: string | AstNode,
importer: string,
options: { attributes: Record<string, string> }
) => ResolveIdResult;
提示
返回类型 ResolveIdResult 与 resolveId
钩子的返回类型相同。
为动态导入定义自定义解析器。返回 false
表示应将导入保持原样,而不传递给其他解析器,从而使其成为外部导入。类似于 resolveId
钩子,您还可以返回一个对象,将导入解析为不同的 id,同时将其标记为外部导入。
attributes
会告诉您导入中存在哪些导入属性。例如,import("foo", {assert: {type: "json"}})
将传递 attributes: {type: "json"}
。
如果将字符串作为参数传递给动态导入,则此钩子返回的字符串将被解释为现有模块 id,而返回 null
将委托给其他解析器,最终委托给 resolveId
。
如果动态导入没有将字符串作为参数传递,则此钩子将访问原始 AST 节点以进行分析,并且在以下方面略有不同
- 如果所有插件都返回
null
,则导入将被视为external
,不会发出警告。 - 如果返回字符串,则此字符串不会被解释为模块 id,而是用作导入参数的替换。插件有责任确保生成的代码有效。
- 要将此类导入解析为现有模块,您仍然可以返回一个对象
{id, external}
。
请注意,此钩子的返回值不会随后传递给 resolveId
;如果您需要访问静态解析算法,可以在插件上下文中使用 this.resolve(source, importer)
。
resolveId
类型 | ResolveIdHook |
---|---|
种类 | async, first |
前一个 | buildStart 如果我们正在解析入口点,则为 moduleParsed 如果我们正在解析导入,或者作为 resolveDynamicImport 的回退。此外,此钩子可以在构建阶段从插件钩子中触发,方法是调用 this.emitFile 来发出入口点,或者在任何时候通过调用 this.resolve 来手动解析 id |
下一个 | load 如果解析的 id 尚未加载,否则为 buildEnd |
type ResolveIdHook = (
source: string,
importer: string | undefined,
options: {
attributes: Record<string, string>;
custom?: { [plugin: string]: any };
isEntry: boolean;
}
) => ResolveIdResult;
type ResolveIdResult = string | null | false | PartialResolvedId;
interface PartialResolvedId {
id: string;
external?: boolean | 'absolute' | 'relative';
attributes?: Record<string, string> | null;
meta?: { [plugin: string]: any } | null;
moduleSideEffects?: boolean | 'no-treeshake' | null;
resolvedBy?: string | null;
syntheticNamedExports?: boolean | string | null;
}
定义自定义解析器。解析器对于例如定位第三方依赖项很有用。这里 source
是导入者,与导入语句中编写的完全相同,即对于
import { foo } from '../bar.js';
源将为 "../bar.js"
。
importer
是导入模块的完全解析的 id。解析入口点时,导入者通常为 undefined
。这里的一个例外是通过 this.emitFile
生成的入口点,因为在这里,您可以提供 importer
参数。
对于这些情况,isEntry
选项将告诉您我们是否正在解析用户定义的入口点、发出的块,或者是否为 this.resolve
上下文函数提供了 isEntry
参数。
例如,您可以将其用作定义入口点自定义代理模块的机制。以下插件将代理所有入口点以注入 polyfill 导入。
// 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.
const resolution = await this.resolve(source, importer, 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;
}
};
}
attributes
会告诉您导入中存在哪些导入属性。例如,import "foo" assert {type: "json"}
将传递 attributes: {type: "json"}
。
返回 null
将委托给其他 resolveId
函数,最终委托给默认解析行为。返回 false
表示应将 source
视为外部模块,而不包含在捆绑包中。如果这对相对导入发生,则 id 将以与使用 external
选项时相同的方式重新规范化。
如果您返回一个对象,则可以将导入解析为不同的 id,同时将其从捆绑包中排除。这使您可以用外部依赖项替换依赖项,而无需用户通过 external
选项手动将其标记为“外部”
function externalizeDependencyPlugin() {
return {
name: 'externalize-dependency',
resolveId(source) {
if (source === 'my-dependency') {
return { id: 'my-dependency-develop', external: true };
}
return null;
}
};
}
如果 external
为 true
,则将根据用户对 makeAbsoluteExternalsRelative
选项的选择将绝对 id 转换为相对 id。可以通过传递 external: "relative"
来始终将绝对 id 转换为相对 id,或者传递 external: "absolute"
来将其保留为绝对 id 来覆盖此选择。返回对象时,相对外部 id(即以 ./
或 ../
开头的 id)不会在内部转换为绝对 id 并在输出中转换回相对 id,而是以不变的形式包含在输出中。如果您希望相对 id 重新规范化和去重,请返回绝对文件系统位置作为 id
并选择 external: "relative"
。
如果第一个解析模块 ID 的钩子返回的 moduleSideEffects
为 false
,并且没有其他模块从该模块导入任何内容,那么即使该模块具有副作用,也不会包含该模块。如果返回 true
,Rollup 将使用其默认算法来包含模块中所有具有副作用的语句(例如,修改全局变量或导出变量)。如果返回 "no-treeshake"
,则将为该模块关闭树摇,并且即使该模块为空,它也将包含在生成的代码块之一中。如果返回 null
或省略该标志,则 moduleSideEffects
将由 treeshake.moduleSideEffects
选项确定,或默认设置为 true
。load
和 transform
钩子可以覆盖此选项。
可以在返回的对象中显式声明 resolvedBy
。它将替换 this.resolve
返回的相应字段。
如果为外部模块返回 attributes
的值,这将决定在生成 "es"
输出时如何呈现该模块的导入。例如,{id: "foo", external: true, attributes: {type: "json"}}
将导致对该模块的导入显示为 import "foo" assert {type: "json"}
。如果未传递值,将使用 attributes
输入参数的值。传递一个空对象以删除所有属性。虽然 attributes
不会影响捆绑模块的呈现,但它们仍然需要在模块的所有导入中保持一致,否则会发出警告。load
和 transform
钩子可以覆盖此选项。
有关 syntheticNamedExports
选项的影响,请参见 合成命名导出。如果返回 null
或省略该标志,则 syntheticNamedExports
将默认为 false
。load
和 transform
钩子可以覆盖此选项。
有关如何使用 meta
选项,请参见 自定义模块元数据。如果返回 null
或省略该选项,则 meta
将默认为一个空对象。load
和 transform
钩子可以添加或替换此对象的属性。
请注意,虽然 resolveId
将针对模块的每个导入调用,因此可以多次解析为相同的 id
,但 external
、attributes
、meta
、moduleSideEffects
或 syntheticNamedExports
的值只能在加载模块之前设置一次。原因是在此调用之后,Rollup 将继续执行该模块的 load
和 transform
钩子,这些钩子可能会覆盖这些值,如果它们这样做,则应该优先考虑。
当通过插件通过 this.resolve
触发此钩子时,可以将自定义选项对象传递给此钩子。虽然此对象将被不加修改地传递,但插件应该遵循添加 custom
属性的约定,该属性包含一个对象,其中键对应于选项 предназначены для. 有关详细信息,请参见 自定义解析器选项。
在监视模式下或显式使用缓存时,缓存模块的解析导入也来自缓存,而不是再次通过 resolveId
钩子确定。要防止这种情况,您可以从该模块的 shouldTransformCachedModule
钩子返回 true
。这将从缓存中删除该模块及其导入解析,并再次调用 transform
和 resolveId
。
shouldTransformCachedModule
类型 | ShouldTransformCachedModuleHook |
---|---|
种类 | async, first |
前一个 | load ,其中加载了缓存文件以将其代码与缓存版本进行比较 |
下一个 | moduleParsed 如果没有插件返回 true ,否则 transform |
type ShouldTransformCachedModuleHook = (options: {
ast: AstNode;
code: string;
id: string;
meta: { [plugin: string]: any };
moduleSideEffects: boolean | 'no-treeshake';
syntheticNamedExports: boolean | string;
}) => boolean | NullValue;
如果使用 Rollup 缓存(例如,在监视模式下或通过 JavaScript API 显式使用),Rollup 将跳过模块的 transform
钩子,如果在 load
钩子之后,加载的 code
与缓存副本的代码相同。要防止这种情况,插件可以实现此钩子并返回 true
,从而丢弃缓存副本,而是转换模块。
此钩子还可以用于找出哪些模块被缓存并访问其缓存的元信息。
如果插件没有返回布尔值,Rollup 将为其他插件触发此钩子,否则将跳过所有剩余的插件。
transform
类型 | (code: string, id: string) => TransformResult |
---|---|
种类 | 异步,顺序 |
前一个 | load ,其中加载了当前处理的文件。如果使用缓存并且存在该模块的缓存副本,则 shouldTransformCachedModule 如果插件为该钩子返回了 true |
下一个 | moduleParsed 一旦文件被处理和解析 |
type TransformResult = string | null | Partial<SourceDescription>;
interface SourceDescription {
code: string;
map?: string | SourceMap;
ast?: ESTree.Program;
attributes?: { [key: string]: string } | null;
meta?: { [plugin: string]: any } | null;
moduleSideEffects?: boolean | 'no-treeshake' | null;
syntheticNamedExports?: boolean | string | null;
}
可用于转换单个模块。为了防止在例如此钩子已经使用 this.parse
生成 AST 的情况下出现额外的解析开销,此钩子可以选择返回一个 { code, ast, map }
对象。ast
必须是具有 start
和 end
属性的标准 ESTree AST。如果转换没有移动代码,您可以通过将 map
设置为 null
来保留现有的源映射。否则,您可能需要生成源映射。请参见 有关源代码转换的部分。
请注意,在监视模式下或显式使用缓存时,此钩子的结果在重建时会被缓存,并且只有在模块的 code
发生更改或上次触发此模块的钩子时添加的文件发生更改时,才会再次触发该钩子。
在所有其他情况下,将触发 shouldTransformCachedModule
钩子,它允许访问缓存的模块。从 shouldTransformCachedModule
返回 true
将从缓存中删除该模块,而是再次调用 transform
。
您还可以使用返回值的对象形式来配置模块的其他属性。请注意,可以只返回属性,而不返回代码转换。
如果 moduleSideEffects
返回 false
,并且没有其他模块从该模块导入任何内容,那么即使该模块具有副作用,也不会包含该模块。
如果返回 true
,Rollup 将使用其默认算法来包含模块中所有具有副作用的语句(例如,修改全局变量或导出变量)。
如果返回 "no-treeshake"
,则将为该模块关闭树摇,并且即使该模块为空,它也将包含在生成的代码块之一中。
如果返回 null
或省略该标志,则 moduleSideEffects
将由加载此模块的 load
钩子、解析此模块的第一个 resolveId
钩子、treeshake.moduleSideEffects
选项确定,或最终默认为 true
。
attributes
包含导入此模块时使用的导入属性。目前,它们不会影响捆绑模块的呈现,而是用于文档目的。如果返回 null
或省略该标志,则 attributes
将由加载此模块的 load
钩子、解析此模块的第一个 resolveId
钩子或此模块的第一个导入中存在的属性确定。
有关 syntheticNamedExports
选项的影响,请参见 合成命名导出。如果返回 null
或省略该标志,则 syntheticNamedExports
将由加载此模块的 load
钩子、解析此模块的第一个 resolveId
钩子、treeshake.moduleSideEffects
选项确定,或最终默认为 false
。
有关如何使用 meta
选项,请参见 自定义模块元数据。如果返回 null
或省略该选项,则 meta
将由加载此模块的 load
钩子、解析此模块的第一个 resolveId
钩子或最终默认为一个空对象确定。
您可以使用 this.getModuleInfo
在此钩子内部找出 attributes
、meta
、moduleSideEffects
和 syntheticNamedExports
的先前值。
watchChange
类型 | watchChange: (id: string, change: {event: 'create' | 'update' | 'delete'}) => void |
---|---|
种类 | async, parallel |
前一个/下一个 | 此钩子可以在构建和输出生成阶段的任何时间触发。如果是这种情况,当前构建将继续进行,但将在当前构建完成后安排新的构建开始,从 options 重新开始 |
每当 Rollup 在 --watch
模式下检测到对监视文件的更改时,都会通知插件。如果返回 Promise,Rollup 将等待 Promise 解析,然后再安排另一个构建。此钩子不能被输出插件使用。第二个参数包含更改事件的更多详细信息。
输出生成钩子
输出生成钩子可以提供有关生成代码块的信息,并在构建完成后修改构建。它们的工作方式和类型与 构建钩子 相同,但针对每次调用 bundle.generate(outputOptions)
或 bundle.write(outputOptions)
分别调用。仅使用输出生成钩子的插件也可以通过输出选项传递,因此仅针对某些输出运行。
输出生成阶段的第一个钩子是 outputOptions
,最后一个钩子是 generateBundle
(如果输出通过 bundle.generate(...)
成功生成)、writeBundle
(如果输出通过 bundle.write(...)
成功生成)或 renderError
(如果在输出生成期间的任何时间发生错误)。
此外,closeBundle
可以作为最后一个钩子调用,但用户有责任手动调用 bundle.close()
来触发此钩子。CLI 将始终确保这种情况发生。
augmentChunkHash
类型 | (chunkInfo: ChunkInfo) => string |
---|---|
种类 | 同步,顺序 |
前一个 | renderChunk |
下一个 | renderChunk 如果还有其他代码块需要处理,否则 generateBundle |
可用于增强单个代码块的哈希值。针对每个 Rollup 输出代码块调用。返回虚假值将不会修改哈希值。真值将传递给 hash.update
。chunkInfo
是 generateBundle
中的简化版本,不包含 code
和 map
,并使用占位符来表示文件名中的哈希值。
以下插件将使用当前时间戳使代码块 foo
的哈希值失效
function augmentWithDatePlugin() {
return {
name: 'augment-with-date',
augmentChunkHash(chunkInfo) {
if (chunkInfo.name === 'foo') {
return Date.now().toString();
}
}
};
}
banner
类型 | string | ((chunk: ChunkInfo) => string) |
---|---|
种类 | 异步,顺序 |
前一个 | resolveFileUrl 用于每个 import.meta.ROLLUP_FILE_URL_referenceId 的使用,以及 resolveImportMeta 用于当前代码块中对 import.meta 的所有其他访问 |
下一个 | renderDynamicImport 用于下一个代码块中的每个动态导入表达式(如果有),否则 renderChunk 用于第一个代码块 |
参见 output.banner/output.footer
。
closeBundle
类型 | closeBundle: () => Promise<void> | void |
---|---|
种类 | async, parallel |
前一个 | buildEnd 如果存在构建错误,否则在调用 bundle.close() 时,在这种情况下,这将是最后一个被触发的钩子 |
可用于清理任何可能正在运行的外部服务。Rollup 的 CLI 会确保在每次运行后调用此钩子,但 JavaScript API 的用户有责任在完成生成捆绑包后手动调用 bundle.close()
。因此,任何依赖此功能的插件都应在其文档中仔细提及这一点。
如果插件希望在 watch 模式下跨构建保留资源,它们可以在此钩子中检查 this.meta.watchMode
并在 closeWatcher
中执行 watch 模式所需的清理。
页脚
类型 | string | ((chunk: ChunkInfo) => string) |
---|---|
种类 | 异步,顺序 |
前一个 | resolveFileUrl 用于每个 import.meta.ROLLUP_FILE_URL_referenceId 的使用,以及 resolveImportMeta 用于当前代码块中对 import.meta 的所有其他访问 |
下一个 | renderDynamicImport 用于下一个代码块中的每个动态导入表达式(如果有),否则 renderChunk 用于第一个代码块 |
参见 output.banner/output.footer
。
生成捆绑包
类型 | (options: OutputOptions, bundle: { [fileName: string]: OutputAsset | OutputChunk }, isWrite: boolean) => void |
---|---|
种类 | 异步,顺序 |
前一个 | augmentChunkHash |
下一个 | writeBundle 如果输出是通过 bundle.write(...) 生成的,否则这是输出生成阶段的最后一个钩子,并且可能再次跟随 outputOptions 如果生成另一个输出。 |
interface OutputAsset {
fileName: string;
name?: string;
needsCodeReference: boolean;
source: string | Uint8Array;
type: 'asset';
}
interface OutputChunk {
code: string;
dynamicImports: string[];
exports: string[];
facadeModuleId: string | null;
fileName: string;
implicitlyLoadedBefore: string[];
imports: string[];
importedBindings: { [imported: string]: string[] };
isDynamicEntry: boolean;
isEntry: boolean;
isImplicitEntry: boolean;
map: SourceMap | null;
modules: {
[id: string]: {
renderedExports: string[];
removedExports: string[];
renderedLength: number;
originalLength: number;
code: string | null;
};
};
moduleIds: string[];
name: string;
preliminaryFileName: string;
referencedFiles: string[];
sourcemapFileName: string | null;
type: 'chunk';
}
在 bundle.generate()
结束时或在 bundle.write()
中写入文件之前立即调用。要修改已写入的文件,请使用 writeBundle
钩子。bundle
提供正在写入或生成的完整文件列表及其详细信息。
您可以在此钩子中从捆绑包对象中删除文件以阻止文件被发出。要发出其他文件,请使用 this.emitFile
插件上下文函数。
危险
不要直接将资产添加到捆绑包中。这会绕过 Rollup 用于跟踪资产的内部机制。它还会导致您的资产丢失 Rollup 在内部依赖的关键属性,并且您的插件可能会在 Rollup 次要版本中出现问题。
相反,始终使用 this.emitFile
。
简介
类型 | string | ((chunk: ChunkInfo) => string) |
---|---|
种类 | 异步,顺序 |
前一个 | resolveFileUrl 用于每个 import.meta.ROLLUP_FILE_URL_referenceId 的使用,以及 resolveImportMeta 用于当前代码块中对 import.meta 的所有其他访问 |
下一个 | renderDynamicImport 用于下一个代码块中的每个动态导入表达式(如果有),否则 renderChunk 用于第一个代码块 |
输出选项
类型 | (outputOptions: OutputOptions) => OutputOptions | null |
---|---|
种类 | 同步,顺序 |
前一个 | buildEnd 如果这是第一次生成输出,否则是 generateBundle 、writeBundle 或 renderError ,具体取决于先前生成的输出。这是输出生成阶段的第一个钩子。 |
下一个 | renderStart |
替换或操作传递给 bundle.generate()
或 bundle.write()
的输出选项对象。返回 null
不会替换任何内容。如果您只需要读取输出选项,建议使用 renderStart
钩子,因为此钩子在所有 outputOptions
钩子的转换都已考虑在内后可以访问输出选项。
结语
类型 | string | ((chunk: ChunkInfo) => string) |
---|---|
种类 | 异步,顺序 |
前一个 | resolveFileUrl 用于每个 import.meta.ROLLUP_FILE_URL_referenceId 的使用,以及 resolveImportMeta 用于当前代码块中对 import.meta 的所有其他访问 |
下一个 | renderDynamicImport 用于下一个代码块中的每个动态导入表达式(如果有),否则 renderChunk 用于第一个代码块 |
渲染块
类型 | RenderChunkHook |
---|---|
种类 | 异步,顺序 |
前一个 | banner 、footer 、intro 、outro 的最后一个块。 |
下一个 | augmentChunkHash |
type RenderChunkHook = (
code: string,
chunk: RenderedChunk,
options: NormalizedOutputOptions,
meta: { chunks: Record<string, RenderedChunk> }
) => { code: string; map?: SourceMapInput } | string | null;
可用于转换单个块。针对每个 Rollup 输出块文件调用。返回 null
将不应用任何转换。如果您在此钩子中更改代码并希望支持源映射,则需要返回一个描述更改的 map
,请参阅 有关源代码转换的部分。
chunk
包含有关块的附加信息,使用与 generateBundle
钩子相同的 ChunkInfo
类型,但存在以下差异。
code
和map
未设置。相反,请使用此钩子的code
参数。- 所有引用的块文件名,其中包含哈希,将包含哈希占位符。这包括
fileName
、imports
、importedBindings
、dynamicImports
和implicitlyLoadedBefore
。当您在此钩子返回的代码中使用此类占位符文件名或其一部分时,Rollup 将在generateBundle
之前将占位符替换为实际的哈希,确保哈希反映最终生成的块的实际内容,包括所有引用的文件哈希。
chunk
是可变的,在此钩子中应用的更改将传播到其他插件和生成的捆绑包。这意味着,如果您在此钩子中添加或删除导入或导出,则应更新 imports
、importedBindings
和/或 exports
。
meta.chunks
包含有关 Rollup 正在生成的所有块的信息,并允许您访问它们的 ChunkInfo
,再次使用哈希的占位符。这意味着您可以在此钩子中探索整个块图。
渲染动态导入
类型 | renderDynamicImportHook |
---|---|
种类 | 同步,第一个 |
前一个 | renderStart 如果这是第一个块,否则是 banner 、footer 、intro 、outro 的前一个块。 |
下一个 | resolveFileUrl 用于每个 import.meta.ROLLUP_FILE_URL_referenceId 的使用,以及 resolveImportMeta 用于当前代码块中对 import.meta 的所有其他访问 |
type renderDynamicImportHook = (options: {
customResolution: string | null;
format: string;
moduleId: string;
targetModuleId: string | null;
}) => { left: string; right: string } | null;
此钩子通过为导入表达式参数左侧 (import(
) 和右侧 ()
) 的代码提供替换,提供对如何渲染动态导入的细粒度控制。返回 null
将委托给此类型的其他钩子,并最终渲染特定于格式的默认值。
format
是渲染的输出格式,moduleId
是执行动态导入的模块的 ID。如果导入可以解析为内部或外部 ID,则 targetModuleId
将设置为此 ID,否则将为 null
。如果动态导入包含一个非字符串表达式,该表达式由 resolveDynamicImport
钩子解析为替换字符串,则 customResolution
将包含该字符串。
以下代码将用自定义处理程序替换所有动态导入,将 import.meta.url
作为第二个参数添加,以允许处理程序正确解析相对导入。
function dynamicImportPolyfillPlugin() {
return {
name: 'dynamic-import-polyfill',
renderDynamicImport() {
return {
left: 'dynamicImportPolyfill(',
right: ', import.meta.url)'
};
}
};
}
// input
import('./lib.js');
// output
dynamicImportPolyfill('./lib.js', import.meta.url);
下一个插件将确保 esm-lib
的所有动态导入都被标记为外部并保留为导入表达式,例如,允许 CommonJS 构建在 Node 13+ 中导入 ES 模块,参见如何在 Node 文档中 从 CommonJS 导入 ES 模块。
function retainImportExpressionPlugin() {
return {
name: 'retain-import-expression',
resolveDynamicImport(specifier) {
if (specifier === 'esm-lib') return false;
return null;
},
renderDynamicImport({ targetModuleId }) {
if (targetModuleId === 'esm-lib') {
return {
left: 'import(',
right: ')'
};
}
}
};
}
请注意,当此钩子在非 ES 格式中重写动态导入时,不会生成任何互操作代码来确保例如默认导出可用作 .default
。插件有责任确保重写的动态导入返回一个 Promise,该 Promise 解析为适当的命名空间对象。
渲染错误
类型 | (error: Error) => void |
---|---|
种类 | async, parallel |
前一个 | 从 renderStart 到 renderChunk 的任何钩子。 |
下一个 | 如果调用它,这是输出生成阶段的最后一个钩子,并且可能再次跟随 outputOptions 如果生成另一个输出。 |
当 Rollup 在 bundle.generate()
或 bundle.write()
期间遇到错误时调用。错误将传递给此钩子。要获得生成成功完成的通知,请使用 generateBundle
钩子。
渲染开始
类型 | (outputOptions: OutputOptions, inputOptions: InputOptions) => void |
---|---|
种类 | async, parallel |
前一个 | outputOptions |
下一个 | renderDynamicImport 针对第一个块中的每个动态导入表达式。 |
每次调用 bundle.generate()
或 bundle.write()
时最初调用。要获得生成完成的通知,请使用 generateBundle
和 renderError
钩子。这是当您需要访问传递给 bundle.generate()
或 bundle.write()
的输出选项时推荐使用的钩子,因为它考虑了所有 outputOptions
钩子的转换,并且还包含未设置选项的正确默认值。它还接收传递给 rollup.rollup()
的输入选项,以便可以作为输出插件使用的插件(即仅使用 generate
阶段钩子的插件)可以访问它们。
解析文件 URL
类型 | ResolveFileUrlHook |
---|---|
种类 | 同步,第一个 |
前一个 | renderDynamicImport 针对当前块中的每个动态导入表达式。 |
下一个 | banner 、footer 、intro 、outro 并行针对当前块。 |
type ResolveFileUrlHook = (options: {
chunkId: string;
fileName: string;
format: InternalModuleFormat;
moduleId: string;
referenceId: string;
relativePath: string;
}) => string | NullValue;
允许自定义 Rollup 如何解析通过 this.emitFile
由插件发出的文件的 URL。默认情况下,Rollup 将为 import.meta.ROLLUP_FILE_URL_referenceId
生成代码,该代码应正确生成发出的文件的绝对 URL,独立于输出格式和部署代码的主机系统。
为此,除 CommonJS 和 UMD 之外的所有格式都假设它们在浏览器环境中运行,其中 URL
和 document
可用。如果失败或要生成更优化的代码,可以使用此钩子自定义此行为。为此,以下信息可用。
chunkId
:引用此文件的块的 ID。如果块文件名包含哈希,则此 ID 将包含占位符。如果它最终出现在生成的代码中,Rollup 将用实际文件名替换此占位符。fileName
:发出的文件的路径和文件名,相对于output.dir
,没有前导./
。同样,如果这是一个在名称中包含哈希的块,它将包含占位符。format
:渲染的输出格式。moduleId
:引用此文件的原始模块的 ID。对有条件地以不同方式解析某些资产很有用。referenceId
:文件的引用 ID。relativePath
:发出的文件的路径和文件名,相对于引用该文件的块。此路径将不包含前导./
,但可能包含前导../
。
以下插件将始终相对于当前文档解析所有文件。
function resolveToDocumentPlugin() {
return {
name: 'resolve-to-document',
resolveFileUrl({ fileName }) {
return `new URL('${fileName}', document.baseURI).href`;
}
};
}
解析导入元数据
类型 | (property: string | null, {chunkId: string, moduleId: string, format: string}) => string | null |
---|---|
种类 | 同步,第一个 |
前一个 | renderDynamicImport 针对当前块中的每个动态导入表达式。 |
下一个 | banner 、footer 、intro 、outro 并行针对当前块。 |
允许自定义 Rollup 如何处理 import.meta
和 import.meta.someProperty
,特别是 import.meta.url
。在 ES 模块中,import.meta
是一个对象,import.meta.url
包含当前模块的 URL,例如,对于浏览器来说是 http://server.net/bundle.js
,对于 Node 来说是 file:///path/to/bundle.js
。
默认情况下,对于除 ES 模块之外的格式,Rollup 将 import.meta.url
替换为尝试匹配此行为的代码,通过返回当前块的动态 URL。请注意,除 CommonJS 和 UMD 之外的所有格式都假设它们在浏览器环境中运行,其中 URL
和 document
可用。对于其他属性,import.meta.someProperty
将被替换为 undefined
,而 import.meta
将被替换为包含 url
属性的对象。
可以通过此钩子更改此行为,包括 ES 模块。对于 import.meta<.someProperty>
的每次出现,此钩子都会使用属性的名称调用,如果直接访问 import.meta
,则为 null
。例如,以下代码将使用原始模块到当前工作目录的相对路径解析 import.meta.url
,并在运行时再次相对于当前文档的基 URL 解析此路径。
function importMetaUrlCurrentModulePlugin() {
return {
name: 'import-meta-url-current-module',
resolveImportMeta(property, { moduleId }) {
if (property === 'url') {
return `new URL('${path.relative(
process.cwd(),
moduleId
)}', document.baseURI).href`;
}
return null;
}
};
}
如果 chunkId
包含哈希,它将包含占位符。如果此占位符最终出现在生成的代码中,Rollup 将用实际的块哈希替换它。
写入捆绑包
类型 | (options: OutputOptions, bundle: { [fileName: string]: AssetInfo | ChunkInfo }) => void |
---|---|
种类 | async, parallel |
前一个 | generateBundle |
下一个 | 如果调用它,这是输出生成阶段的最后一个钩子,并且可能再次跟随 outputOptions 如果生成另一个输出。 |
仅在 bundle.write()
结束时调用,所有文件都已写入。类似于 generateBundle
钩子,bundle
提供正在写入的完整文件列表及其详细信息。
插件上下文
许多实用函数和信息位可以通过this
在大多数钩子中访问
this.addWatchFile
类型 | (id: string) => void |
---|
在监视模式下添加要监视的额外文件,以便对这些文件的更改会触发重建。id
可以是文件的绝对路径或目录路径,也可以是相对于当前工作目录的路径。此上下文函数可以在除closeBundle
之外的所有插件钩子中使用。但是,如果watch.skipWrite
设置为true
,则在输出生成钩子中使用时,它将不起作用。
注意:通常在监视模式下,为了提高重建速度,transform
钩子只会针对其内容实际发生更改的给定模块触发。在transform
钩子中使用this.addWatchFile
将确保如果监视的文件发生更改,transform
钩子也会针对此模块重新评估。
一般来说,建议在依赖于监视文件的钩子中使用this.addWatchFile
。
this.debug
类型 | (log: string | RollupLog | (() => RollupLog | string), position?: number | { column: number; line: number }) => void |
---|
生成一个"debug"
日志。有关详细信息,请参阅this.warn
。调试日志始终会由 Rollup 添加code: "PLUGIN_LOG"
。确保为这些日志添加一个独特的pluginCode
,以便于过滤。
这些日志只有在logLevel
选项显式设置为"debug"
时才会被处理,否则它不会执行任何操作。因此,鼓励在插件中添加有用的调试日志,因为这有助于发现问题,同时默认情况下会有效地静音。如果您需要进行昂贵的计算来生成日志,请确保使用函数形式,以便只有在实际处理日志时才会执行这些计算。
function plugin() {
return {
name: 'test',
transform(code, id) {
this.debug(
() =>
`transforming ${id},\n` +
`module contains, ${code.split('\n').length} lines`
);
}
};
}
this.emitFile
类型 | (emittedFile: EmittedChunk | EmittedPrebuiltChunk | EmittedAsset) => string |
---|
interface EmittedChunk {
type: 'chunk';
id: string;
name?: string;
fileName?: string;
implicitlyLoadedAfterOneOf?: string[];
importer?: string;
preserveSignature?: 'strict' | 'allow-extension' | 'exports-only' | false;
}
interface EmittedPrebuiltChunk {
type: 'prebuilt-chunk';
fileName: string;
code: string;
exports?: string[];
map?: SourceMap;
}
interface EmittedAsset {
type: 'asset';
name?: string;
needsCodeReference?: boolean;
fileName?: string;
source?: string | Uint8Array;
}
发出一个包含在构建输出中的新文件,并返回一个referenceId
,该referenceId
可以在各种地方用来引用发出的文件。您可以发出块、预构建块或资产。
发出块或资产时,可以提供name
或fileName
。如果提供了fileName
,它将被用作生成的 文件的名称,如果这会导致冲突,则会抛出错误。否则,如果提供了name
,它将用作相应output.chunkFileNames
或output.assetFileNames
模式中[name]
的替换,可能会在文件名末尾添加一个唯一的数字以避免冲突。如果既没有提供name
也没有提供fileName
,则将使用默认名称。预构建块必须始终具有fileName
。
您可以通过import.meta.ROLLUP_FILE_URL_referenceId
引用由load
或transform
插件钩子返回的任何代码中发出的文件的 URL。有关更多详细信息和示例,请参阅文件 URL。
替换import.meta.ROLLUP_FILE_URL_referenceId
的生成代码可以通过resolveFileUrl
插件钩子自定义。您还可以使用this.getFileName(referenceId)
来确定文件名,只要它可用即可。如果文件名没有显式设置,那么
- 资产文件名从
renderStart
钩子开始可用。对于稍后发出的资产,文件名将在发出资产后立即可用。 - 不包含哈希的块文件名在
renderStart
钩子之后创建块时立即可用。 - 如果块文件名包含哈希,则在
generateBundle
之前的任何钩子中使用getFileName
将返回一个包含占位符的名称,而不是实际名称。如果您在renderChunk
中转换的块中使用此文件名或其部分,Rollup 将在generateBundle
之前用实际哈希替换占位符,确保哈希反映最终生成的块的实际内容,包括所有引用的文件哈希。
如果type
是chunk
,那么这将发出一个新的块,其给定的模块id
作为入口点。要解析它,id
将像常规入口点一样通过构建钩子传递,从resolveId
开始。如果提供了importer
,它将充当resolveId
的第二个参数,对于正确解析相对路径很重要。如果没有提供,路径将相对于当前工作目录解析。如果提供了preserveSignature
的值,它将覆盖此特定块的preserveEntrySignatures
。
这不会导致图中出现重复的模块,而是如果需要,将拆分现有块或创建具有重新导出的外观块。具有指定fileName
的块将始终生成单独的块,而其他发出的块可能会与现有块重复,即使name
不匹配。如果这样的块没有重复,则将使用output.chunkFileNames
名称模式。
默认情况下,Rollup 假设发出的块独立于其他入口点执行,甚至可能在任何其他代码执行之前执行。这意味着,如果发出的块与现有入口点共享一个依赖项,Rollup 将为在这些入口点之间共享的依赖项创建一个额外的块。为implicitlyLoadedAfterOneOf
提供一个非空模块 ID 数组将改变这种行为,通过向 Rollup 提供更多信息来防止这种情况在某些情况下发生。这些 ID 将以与id
属性相同的方式解析,如果提供了importer
属性,则会尊重它。Rollup 现在将假设只有在导致implicitlyLoadedAfterOneOf
中的一个 ID 加载的入口点中的至少一个已执行后,才会执行发出的块,从而创建与新发出的块仅可通过implicitlyLoadedAfterOneOf
中的模块的动态导入访问时相同的块。以下是一个使用它来创建一个包含多个脚本的简单 HTML 文件的示例,创建优化的块以尊重它们的执行顺序
// rollup.config.js
function generateHtmlPlugin() {
let ref1, ref2, ref3;
return {
name: 'generate-html',
buildStart() {
ref1 = this.emitFile({
type: 'chunk',
id: 'src/entry1'
});
ref2 = this.emitFile({
type: 'chunk',
id: 'src/entry2',
implicitlyLoadedAfterOneOf: ['src/entry1']
});
ref3 = this.emitFile({
type: 'chunk',
id: 'src/entry3',
implicitlyLoadedAfterOneOf: ['src/entry2']
});
},
generateBundle() {
this.emitFile({
type: 'asset',
fileName: 'index.html',
source: `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script src="${this.getFileName(ref1)}" type="module"></script>
<script src="${this.getFileName(ref2)}" type="module"></script>
<script src="${this.getFileName(ref3)}" type="module"></script>
</body>
</html>`
});
}
};
}
export default {
input: [],
preserveEntrySignatures: false,
plugins: [generateHtmlPlugin()],
output: {
format: 'es',
dir: 'dist'
}
};
如果没有动态导入,这将创建正好三个块,其中第一个块包含src/entry1
的所有依赖项,第二个块仅包含src/entry2
的依赖项,这些依赖项不包含在第一个块中,从第一个块导入这些依赖项,第三个块也是如此。
请注意,即使implicitlyLoadedAfterOneOf
中可以使用任何模块 ID,但如果这样的 ID 无法与块唯一关联,Rollup 将抛出错误,例如,因为id
无法从现有的静态入口点隐式或显式访问,或者因为文件被完全树状摇动。但是,仅使用入口点(由用户定义或先前发出的块的入口点)将始终有效。
如果type
是prebuilt-chunk
,它将发出一个块,该块具有由code
参数提供的固定内容。目前,fileName
也需要提供块的名称。如果它导出了一些变量,我们应该通过可选的exports
列出这些变量。通过map
,我们可以提供与code
相对应的源映射。
要在导入中引用预构建块,我们需要在resolveId
钩子中将“模块”标记为外部,因为预构建块不是模块图的一部分。相反,它们的行为类似于具有块元数据的资产
function emitPrebuiltChunkPlugin() {
return {
name: 'emit-prebuilt-chunk',
resolveId(source) {
if (source === './my-prebuilt-chunk.js') {
return {
id: source,
external: true
};
}
},
buildStart() {
this.emitFile({
type: 'prebuilt-chunk',
fileName: 'my-prebuilt-chunk.js',
code: 'export const foo = "foo"',
exports: ['foo']
});
}
};
}
然后,您可以在代码中引用预构建块
import { foo } from './my-prebuilt-chunk.js';
目前,发出预构建块是一个基本功能。期待您的反馈。
如果type
是asset
,那么这将发出一个任意的新文件,其给定的source
作为内容。可以通过this.setAssetSource(referenceId, source)
将设置source
推迟到以后的时间,以便能够在构建阶段引用文件,同时在生成阶段为每个输出分别设置源。具有指定fileName
的资产将始终生成单独的文件,而其他发出的资产如果具有相同的源,即使name
不匹配,也可能会与现有资产重复。如果一个没有fileName
的资产没有重复,则将使用output.assetFileNames
名称模式。如果needsCodeReference
设置为true
,并且此资产没有通过import.meta.ROLLUP_FILE_URL_referenceId
被输出中的任何代码引用,那么 Rollup 不会发出它。这也尊重通过树状摇动删除的引用,即,如果相应的import.meta.ROLLUP_FILE_URL_referenceId
是源代码的一部分,但实际上没有使用,并且引用被树状摇动删除,那么资产不会发出。
this.error
类型 | (error: string | RollupLog | Error, position?: number | { column: number; line: number }) => never |
---|
在结构上等效于this.warn
,只是它还会使用错误中止捆绑过程。有关RollupLog
类型的详细信息,请参阅onLog
选项。
如果传递了 Error 实例,它将按原样使用,否则将使用给定的错误消息和所有其他提供的属性创建一个新的 Error 实例。
在除onLog
钩子之外的所有钩子中,错误将被添加code: "PLUGIN_ERROR"
和plugin: plugin.name
属性。如果code
属性已经存在,并且代码不以PLUGIN_
开头,它将被重命名为pluginCode
。
在onLog
钩子中,此函数是一种将警告转换为错误的简单方法,同时保留警告的所有附加属性
function myPlugin() {
return {
name: 'my-plugin',
onLog(level, log) {
if (level === 'warn' && log.code === 'THIS_IS_NOT_OK') {
return this.error(log);
}
}
};
}
在transform
钩子中使用时,当前模块的id
也将被添加,并且可以提供position
。这是一个字符索引或文件位置,它将用于使用pos
、loc
(一个标准的{ file, line, column }
对象)和frame
(显示位置的代码片段)来增强日志。
this.getCombinedSourcemap
类型 | () => SourceMap |
---|
获取所有先前插件的组合源映射。此上下文函数只能在transform
插件钩子中使用。
this.getFileName
类型 | (referenceId: string) => string |
---|
获取通过this.emitFile
发出的块或资产的文件名。文件名将相对于outputOptions.dir
。
this.getModuleIds
类型 | () => IterableIterator<string> |
---|
返回一个Iterator
,它可以访问当前图中的所有模块 ID。它可以通过以下方式迭代
for (const moduleId of this.getModuleIds()) {
/* ... */
}
或通过Array.from(this.getModuleIds())
转换为数组。
this.getModuleInfo
类型 | (moduleId: string) => (ModuleInfo | null) |
---|
interface ModuleInfo {
id: string; // the id of the module, for convenience
code: string | null; // the source code of the module, `null` if external or not yet available
ast: ESTree.Program; // the parsed abstract syntax tree if available
hasDefaultExport: boolean | null; // is there a default export, `null` if external or not yet available
isEntry: boolean; // is this a user- or plugin-defined entry point
isExternal: boolean; // for external modules that are referenced but not included in the graph
isIncluded: boolean | null; // is the module included after tree-shaking, `null` if external or not yet available
importedIds: string[]; // the module ids statically imported by this module
importedIdResolutions: ResolvedId[]; // how statically imported ids were resolved, for use with this.load
importers: string[]; // the ids of all modules that statically import this module
exportedBindings: Record<string, string[]> | null; // contains all exported variables associated with the path of `from`, `null` if external
exports: string[] | null; // all exported variables, `null` if external
dynamicallyImportedIds: string[]; // the module ids imported by this module via dynamic import()
dynamicallyImportedIdResolutions: ResolvedId[]; // how ids imported via dynamic import() were resolved
dynamicImporters: string[]; // the ids of all modules that import this module via dynamic import()
implicitlyLoadedAfterOneOf: string[]; // implicit relationships, declared via this.emitFile
implicitlyLoadedBefore: string[]; // implicit relationships, declared via this.emitFile
attributes: { [key: string]: string }; // import attributes for this module
meta: { [plugin: string]: any }; // custom module meta-data
moduleSideEffects: boolean | 'no-treeshake'; // are imports of this module included if nothing is imported from it
syntheticNamedExports: boolean | string; // final value of synthetic named exports
}
interface ResolvedId {
id: string; // the id of the imported module
external: boolean | 'absolute'; // is this module external, "absolute" means it will not be rendered as relative in the module
attributes: { [key: string]: string }; // import attributes for this import
meta: { [plugin: string]: any }; // custom module meta-data when resolving the module
moduleSideEffects: boolean | 'no-treeshake'; // are side effects of the module observed, is tree-shaking enabled
resolvedBy: string; // which plugin resolved this module, "rollup" if resolved by Rollup itself
syntheticNamedExports: boolean | string; // does the module allow importing non-existing named exports
}
返回有关所述模块的附加信息。
在构建过程中,此对象表示当前可用的有关模块的信息,这些信息在buildEnd
钩子之前可能不准确
id
和isExternal
永远不会改变。code
、ast
、hasDefaultExport
、exports
和exportedBindings
仅在解析后可用,即在moduleParsed
钩子中或在等待this.load
之后。在那一点上,它们将不再改变。- 如果
isEntry
为true
,它将不再改变。但是,模块在解析后可以通过this.emitFile
或插件通过this.load
在解析入口点时的resolveId
钩子检查潜在的入口点,从而成为入口点。因此,不建议在transform
钩子中依赖此标志。它将在buildEnd
后不再改变。 - 类似地,
implicitlyLoadedAfterOneOf
可以通过this.emitFile
在buildEnd
之前随时接收额外的条目。 importers
、dynamicImporters
和implicitlyLoadedBefore
将以空数组开始,当发现新的导入者和隐式依赖项时,它们将接收额外的条目。它们将在buildEnd
后不再改变。isIncluded
仅在buildEnd
后可用,此时它将不再改变。importedIds
、importedIdResolutions
、dynamicallyImportedIds
和dynamicallyImportedIdResolutions
在模块解析并解析其依赖项后可用。这种情况发生在moduleParsed
钩子中或在使用resolveDependencies
标志等待this.load
之后。此时,它们将不再改变。attributes
、meta
、moduleSideEffects
和syntheticNamedExports
可以通过load
和transform
钩子改变。此外,虽然大多数属性是只读的,但这些属性是可写的,如果在触发buildEnd
钩子之前发生更改,则会拾取这些更改。meta
本身不应被覆盖,但在任何时候修改其属性以存储有关模块的元信息是可以的。这样做而不是在插件中保持状态的优势在于,如果使用meta
,例如在从 CLI 使用监视模式时,它将被持久化到缓存中并从缓存中恢复。
如果找不到模块 ID,则返回 null
。
this.getWatchFiles
类型 | () => string[] |
---|
获取先前已监视的文件的 ID。包括插件使用 this.addWatchFile
添加的文件和 Rollup 在构建过程中隐式添加的文件。
this.info
类型 | (log: string | RollupLog | (() => RollupLog | string), position?: number | { column: number; line: number }) => void |
---|
生成一个 "info"
日志。有关详细信息,请参见 this.warn
。信息日志始终由 Rollup 添加 code: "PLUGIN_LOG"
。由于这些日志默认情况下会显示,因此将它们用于不是警告但对所有用户在每次构建时显示都有意义的信息。
如果 logLevel
选项设置为 "warn"
或 "silent"
,则此方法将不执行任何操作。
this.load
类型 | 加载 |
---|
type Load = (options: {
id: string;
resolveDependencies?: boolean;
attributes?: Record<string, string> | null;
meta?: CustomPluginOptions | null;
moduleSideEffects?: boolean | 'no-treeshake' | null;
syntheticNamedExports?: boolean | string | null;
}) => Promise<ModuleInfo>;
加载并解析与给定 ID 对应的模块,如果提供,则将附加元信息附加到模块。这将触发与模块被另一个模块导入时触发的相同的 load
、transform
和 moduleParsed
钩子。
这允许您在决定如何在 resolveId
钩子中解析模块之前检查模块的最终内容,例如解析为代理模块。如果模块稍后成为图的一部分,则使用此上下文函数不会产生额外的开销,因为模块不会再次解析。只要签名既不是 null
也不外部,您就可以直接将 this.resolve
的返回值传递给此函数。
返回的 Promise 将在模块完全转换和解析后但任何导入解析之前解析。这意味着生成的 ModuleInfo
将具有空的 importedIds
、dynamicallyImportedIds
、importedIdResolutions
和 dynamicallyImportedIdResolutions
。这有助于避免在 resolveId
钩子中等待 this.load
时出现死锁情况。如果您对 importedIds
和 dynamicallyImportedIds
感兴趣,您可以实现 moduleParsed
钩子或传递 resolveDependencies
标志,这将使 this.load
返回的 Promise 等待直到所有依赖项 ID 都已解析。
请注意,关于 attributes
、meta
、moduleSideEffects
和 syntheticNamedExports
选项,与 resolveId
钩子相同的限制适用:它们的值只有在模块尚未加载时才有效。因此,使用 this.resolve
首先找出是否有任何插件希望在其 resolveId
钩子中为这些选项设置特殊值,并在适当的情况下将这些选项传递给 this.load
非常重要。下面的示例展示了如何处理此问题以添加一个用于包含特殊代码注释的模块的代理模块。请注意对重新导出默认导出的特殊处理
export default function addProxyPlugin() {
return {
async resolveId(source, importer, options) {
if (importer?.endsWith('?proxy')) {
// Do not proxy ids used in proxies
return null;
}
// We make sure to pass on any resolveId options to
// this.resolve to get the module id
const resolution = await this.resolve(source, importer, options);
// We can only pre-load existing and non-external ids
if (resolution && !resolution.external) {
// we pass on the entire resolution information
const moduleInfo = await this.load(resolution);
if (moduleInfo.code.includes('/* use proxy */')) {
return `${resolution.id}?proxy`;
}
}
// As we already fully resolved the module, there is no reason
// to resolve it again
return resolution;
},
load(id) {
if (id.endsWith('?proxy')) {
const importee = id.slice(0, -'?proxy'.length);
// Note that namespace reexports do not reexport default
// exports
let code = `console.log('proxy for ${importee}'); export * from ${JSON.stringify(
importee
)};`;
// We know that while resolving the proxy, importee was
// already fully loaded and parsed, so we can rely on
// hasDefaultExport
if (this.getModuleInfo(importee).hasDefaultExport) {
code += `export { default } from ${JSON.stringify(importee)};`;
}
return code;
}
return null;
}
};
}
如果模块已加载,this.load
将只等待解析完成,然后返回其模块信息。如果模块尚未被另一个模块导入,它不会自动触发加载此模块导入的其他模块。相反,静态和动态依赖项只有在该模块至少被导入一次后才会被加载。
虽然在 resolveId
钩子中使用 this.load
是安全的,但在 load
或 transform
钩子中等待它时要非常小心。如果模块图中存在循环依赖关系,这很容易导致死锁,因此任何插件都需要手动注意避免在加载模块的 load
或 transform
中等待 this.load
。
这是一个更详细的示例,我们通过 resolveDependencies
选项和对 this.load
的重复调用扫描整个依赖子图。我们使用一个已处理模块 ID 的 Set
来处理循环依赖关系。插件的目标是向每个动态导入的块添加一个日志,该日志只列出块中的所有模块。虽然这只是一个玩具示例,但该技术可以用于例如为子图中导入的所有 CSS 创建单个样式标签。
// The leading \0 instructs other plugins not to try to resolve, load or
// transform our proxy modules
const DYNAMIC_IMPORT_PROXY_PREFIX = '\0dynamic-import:';
export default function dynamicChunkLogsPlugin() {
return {
name: 'dynamic-chunk-logs',
async resolveDynamicImport(specifier, importer) {
// Ignore non-static targets
if (!(typeof specifier === 'string')) return;
// Get the id and initial meta information of the import target
const resolved = await this.resolve(specifier, importer);
// Ignore external targets. Explicit externals have the
// "external" property while unresolved imports are "null".
if (resolved && !resolved.external) {
// We trigger loading the module without waiting for it
// here because meta information attached by resolveId
// hooks, that may be contained in "resolved" and that
// plugins like "commonjs" may depend upon, is only
// attached to a module the first time it is loaded. This
// ensures that this meta information is not lost when we
// later use "this.load" again in the load hook with just
// the module id.
this.load(resolved);
return `${DYNAMIC_IMPORT_PROXY_PREFIX}${resolved.id}`;
}
},
async load(id) {
// Ignore all files except our dynamic import proxies
if (!id.startsWith('\0dynamic-import:')) return null;
const actualId = id.slice(DYNAMIC_IMPORT_PROXY_PREFIX.length);
// To allow loading modules in parallel while keeping
// complexity low, we do not directly await each "this.load"
// call but put their promises into an array where we await
// them via an async for loop.
const moduleInfoPromises = [
this.load({ id: actualId, resolveDependencies: true })
];
// We track each loaded dependency here so that we do not load
// a file twice and also do not get stuck when there are
// circular dependencies.
const dependencies = new Set([actualId]);
// "importedIdResolutions" tracks the objects created by
// resolveId hooks. We are using those instead of "importedIds"
// so that again, important meta information is not lost.
for await (const { importedIdResolutions } of moduleInfoPromises) {
for (const resolved of importedIdResolutions) {
if (!dependencies.has(resolved.id)) {
dependencies.add(resolved.id);
moduleInfoPromises.push(
this.load({ ...resolved, resolveDependencies: true })
);
}
}
}
// We log all modules in a dynamic chunk when it is loaded.
let code = `console.log([${[...dependencies]
.map(JSON.stringify)
.join(', ')}]); export * from ${JSON.stringify(actualId)};`;
// Namespace reexports do not reexport default exports, which
// is why we reexport it manually if it exists
if (this.getModuleInfo(actualId).hasDefaultExport) {
code += `export { default } from ${JSON.stringify(actualId)};`;
}
return code;
}
};
}
this.meta
类型 | {rollupVersion: string, watchMode: boolean} |
---|
包含可能有用 Rollup 元数据的对象
rollupVersion
:在package.json
中定义的当前运行的 Rollup 版本。watchMode
:如果 Rollup 是通过rollup.watch(...)
或从命令行使用--watch
启动的,则为true
,否则为false
。
meta
是唯一可以从 options
钩子访问的上下文属性。
this.parse
类型 | (code: string, options?: ParseOptions) => ESTree.Program |
---|
interface ParseOptions {
allowReturnOutsideFunction?: boolean;
}
使用 Rollup 的内部基于 SWC 的解析器将代码解析为 ESTree 兼容 的 AST。
allowReturnOutsideFunction
:当true
时,这允许 return 语句在函数之外,例如支持解析 CommonJS 代码。
this.resolve
类型 | 解析 |
---|
type Resolve = (
source: string,
importer?: string,
options?: {
skipSelf?: boolean;
isEntry?: boolean;
attributes?: { [key: string]: string };
custom?: { [plugin: string]: any };
}
) => ResolvedId;
提示
此钩子的返回类型 ResolvedId 在 this.getModuleInfo
中定义。
使用 Rollup 使用的相同插件将导入解析为模块 ID(即文件名),并确定导入是否应为外部。如果返回 null
,则导入无法由 Rollup 或任何插件解析,但用户没有明确将其标记为外部。如果返回一个绝对外部 ID,该 ID 应该通过 makeAbsoluteExternalsRelative
选项或通过 resolveId
钩子中的显式插件选择在输出中保持绝对,则 external
将为 "absolute"
而不是 true
。
skipSelf
的默认值为 true
,因此调用 this.resolve
的插件的 resolveId
钩子在解析时将被跳过。当其他插件在处理原始 this.resolve
调用时,在其 resolveId
钩子中使用完全相同的 source
和 importer
也调用 this.resolve
时,原始插件的 resolveId
钩子也将被跳过。这里的理由是,插件已经声明它在此时“不知道”如何解析 source
和 importer
的这种特定组合。如果您不希望这种行为,请将 skipSelf
设置为 false
,并在必要时实现您自己的无限循环预防机制。
您还可以通过 custom
选项传递一个插件特定选项的对象,有关详细信息,请参见 自定义解析器选项。
您在此处传递的 isEntry
值将传递到处理此调用的 resolveId
钩子,否则如果存在导入者,则传递 false
,如果不存在,则传递 true
。
如果您为 attributes
传递一个对象,它将模拟使用断言解析导入,例如 attributes: {type: "json"}
模拟解析 import "foo" assert {type: "json"}
。这将传递给处理此调用的任何 resolveId
钩子,并最终可能成为返回对象的一部分。
从 resolveId
钩子调用此函数时,您应该始终检查是否适合您传递 isEntry
、custom
和 attributes
选项。
resolvedBy
的值指的是哪个插件解析了此源。如果它是由 Rollup 本身解析的,则该值将为“rollup”。如果插件中的 resolveId
钩子解析了此源,则该值将为插件的名称,除非它为 resolvedBy
返回了一个显式值。此标志仅用于调试和文档目的,不会被 Rollup 进一步处理。
this.setAssetSource
类型 | (referenceId: string, source: string | Uint8Array) => void |
---|
设置资产的延迟源。请注意,您也可以将 Node Buffer
作为 source
传递,因为它是的子类 Uint8Array
。
this.warn
类型 | (log: string | RollupLog | (() => RollupLog | string), position?: number | { column: number; line: number }) => void |
---|
使用此方法将为构建生成警告,这些警告是日志级别为 "warn"
的日志。有关 RollupLog
类型的详细信息,请参见 onLog
选项。要生成其他日志,请参见 this.info
和 this.debug
。要生成错误,请参见 this.error
。
与内部生成的警告一样,这些日志将首先传递给插件 onLog
钩子并由其过滤,然后转发到自定义 onLog
或 onwarn
处理程序或打印到控制台。
warning
参数可以是 string
或一个对象,该对象至少具有一个 message
属性
this.warn('hmm...');
// is equivalent to
this.warn({
message: 'hmm...',
pluginCode: 'CODE_TO_IDENTIFY_LOG',
meta: 'Additional plugin specific information'
});
我们鼓励您使用具有 pluginCode
属性的对象,因为这将允许用户在 onLog
处理程序中轻松过滤这些日志。如果您需要添加其他信息,可以使用 meta
属性。如果日志包含 code
并且还没有 pluginCode
属性,它将被重命名为 pluginCode
,因为插件警告始终由 Rollup 添加 code
为 PLUGIN_WARNING
。要防止这种行为,插件可以使用传递给 buildStart
钩子的规范化 onLog
选项。从插件调用此选项不会在将日志传递给插件 onLog
处理程序和 onLog
或 onwarn
处理程序时更改属性。
如果您需要进行昂贵的计算来生成日志,您还可以传递一个返回 string
或 RollupLog
对象的函数。此函数只有在日志未被 logLevel
选项过滤时才会被调用。
// This will only run if the logLevel is set to "debug"
this.debug(() => generateExpensiveDebugLog());
在transform
钩子中使用时,当前模块的id
也将被添加,并且可以提供position
。这是一个字符索引或文件位置,它将用于使用pos
、loc
(一个标准的{ file, line, column }
对象)和frame
(显示位置的代码片段)来增强日志。
如果 logLevel
选项设置为 "silent"
,则此方法将不执行任何操作。
文件 URL
要在 JS 代码中引用文件 URL 引用,请使用 import.meta.ROLLUP_FILE_URL_referenceId
替换。这将生成依赖于输出格式的代码,并生成指向目标环境中已发出文件的 URL。请注意,除了 CommonJS 和 UMD 之外的所有格式都假设它们在浏览器环境中运行,其中 URL
和 document
可用。
以下示例将检测 .svg
文件的导入,将导入的文件作为资产发出,并返回它们的 URL 以用作 img
标签的 src
属性等
function svgResolverPlugin() {
return {
name: 'svg-resolver',
resolveId(source, importer) {
if (source.endsWith('.svg')) {
return path.resolve(path.dirname(importer), source);
}
},
load(id) {
if (id.endsWith('.svg')) {
const referenceId = this.emitFile({
type: 'asset',
name: path.basename(id),
source: fs.readFileSync(id)
});
return `export default import.meta.ROLLUP_FILE_URL_${referenceId};`;
}
}
};
}
用法
import logo from '../images/logo.svg';
const image = document.createElement('img');
image.src = logo;
document.body.appendChild(image);
有时,引用此资产的代码仅在以下示例中条件使用
import logo from '../images/logo.svg';
if (COMPILER_FLAG) {
const image = document.createElement('img');
image.src = logo;
document.body.appendChild(image);
}
如果插件将 COMPILER_FLAG
替换为 false
,那么我们将得到一个意外的结果:未引用的资产仍然发出,但未被使用。我们可以通过在调用 this.emitFile
时将 needsCodeReference
设置为 true 来解决此问题,如下面的代码所示
function svgResolverPlugin() {
return {
/* ... */
load(id) {
if (id.endsWith('.svg')) {
const referenceId = this.emitFile({
type: 'asset',
name: path.basename(id),
needsCodeReference: true,
source: fs.readFileSync(id)
});
return `export default import.meta.ROLLUP_FILE_URL_${referenceId};`;
}
}
};
}
现在,只有当代码中实际使用引用 import.meta.ROLLUP_FILE_URL_referenceId
时,资产才会被添加到捆绑包中。
与资产类似,发出的块也可以通过 import.meta.ROLLUP_FILE_URL_referenceId
从 JS 代码中引用。
以下示例将检测以 register-paint-worklet:
为前缀的导入,并生成必要的代码和单独的块以生成 CSS paint worklet。请注意,这仅在现代浏览器中有效,并且仅在输出格式设置为 es
时有效。
const REGISTER_WORKLET = 'register-paint-worklet:';
function registerPaintWorkletPlugin() {
return {
name: 'register-paint-worklet',
load(id) {
if (id.startsWith(REGISTER_WORKLET)) {
return `CSS.paintWorklet.addModule(import.meta.ROLLUP_FILE_URL_${this.emitFile(
{
type: 'chunk',
id: id.slice(REGISTER_WORKLET.length)
}
)});`;
}
},
resolveId(source, importer) {
// We remove the prefix, resolve everything to absolute ids and
// add the prefix again. This makes sure that you can use
// relative imports to define worklets
if (source.startsWith(REGISTER_WORKLET)) {
return this.resolve(
source.slice(REGISTER_WORKLET.length),
importer
).then(resolvedId => REGISTER_WORKLET + resolvedId.id);
}
return null;
}
};
}
用法
// main.js
import 'register-paint-worklet:./worklet.js';
import { color, size } from './config.js';
document.body.innerHTML += `<h1 style="background-image: paint(vertical-lines);">color: ${color}, size: ${size}</h1>`;
// worklet.js
import { color, size } from './config.js';
registerPaint(
'vertical-lines',
class {
paint(ctx, geom) {
for (let x = 0; x < geom.width / size; x++) {
ctx.beginPath();
ctx.fillStyle = color;
ctx.rect(x * size, 0, 2, geom.height);
ctx.fill();
}
}
}
);
// config.js
export const color = 'greenyellow';
export const size = 6;
如果构建此代码,主块和 worklet 将通过共享块共享来自 config.js
的代码。这使我们能够利用浏览器缓存来减少传输的数据并加快 worklet 的加载速度。
转换器
转换器插件(即那些为例如转译非 JS 文件返回 transform
函数的插件)应支持 options.include
和 options.exclude
,它们都可以是 minimatch 模式或 minimatch 模式的数组。如果省略 options.include
或其长度为零,则应默认包含文件;否则,只有在 ID 与其中一个模式匹配时才应包含它们。
如果 transform
钩子返回一个对象,它还可以包含一个 ast
属性。只有在您知道自己在做什么的情况下才使用此功能。请注意,只有转换链中的最后一个 AST 将被使用(如果有转换,则 load
钩子生成的任何 AST 将被丢弃以用于转换的模块。)
示例转换器
(使用 @rollup/pluginutils 来获取常用功能,并以推荐的方式实现转换器。)
import { createFilter } from '@rollup/pluginutils';
function transformCodePlugin(options = {}) {
const filter = createFilter(options.include, options.exclude);
return {
name: 'transform-code',
transform(code, id) {
if (!filter(id)) return;
// proceed with the transformation...
return {
code: generatedCode,
map: generatedSourceMap
};
}
};
}
源代码转换
如果插件转换源代码,它应该自动生成源映射,除非有特定的 sourceMap: false
选项。Rollup 只关心 mappings
属性(其他所有内容都由 Rollup 自动处理)。magic-string 提供了一种简单的方法来为添加或删除代码片段等基本转换生成此类映射。
如果生成源映射没有意义(例如 rollup-plugin-string),则返回一个空源映射
return {
code: transformedCode,
map: { mappings: '' }
};
如果转换不移动代码,您可以通过返回 null
来保留现有的源映射
return {
code: transformedCode,
map: null
};
如果您创建了一个您认为对其他人有用的插件,请将其发布到 NPM 并将其提交到 github.com/rollup/awesome!
合成命名导出
可以通过在 resolveId
、load
或 transform
钩子中为模块设置 syntheticNamedExports
选项来指定缺失导出的回退导出。如果对 syntheticNamedExports
使用字符串值,则此模块将回退任何缺失的命名导出的解析到给定名称的命名导出的属性
dep.js: ({syntheticNamedExports: '__synthetic'}
)
export const foo = 'explicit';
export const __synthetic = {
foo: 'foo',
bar: 'bar'
};
main.js
import { foo, bar, baz, __synthetic } from './dep.js';
// logs "explicit" as non-synthetic exports take precedence
console.log(foo);
// logs "bar", picking the property from __synthetic
console.log(bar);
// logs "undefined"
console.log(baz);
// logs "{foo:'foo',bar:'bar'}"
console.log(__synthetic);
当用作入口点时,只有显式导出才会被公开。合成回退导出(即示例中的 __synthetic
)不会为 syntheticNamedExports
的字符串值公开。但是,如果该值为 true
,则默认导出将被公开。这是 syntheticNamedExports: true
和 syntheticNamedExports: 'default'
之间唯一的显著区别。
插件间通信
在使用许多专用插件时,在某些时候,可能需要无关的插件能够在构建过程中交换信息。Rollup 提供了几种机制来实现这一点。
自定义解析器选项
假设您有一个插件,它应该根据另一个插件如何生成导入来将导入解析为不同的 ID。实现此目的的一种方法是重写导入以使用特殊的代理 ID,例如,通过 require("foo")
在 CommonJS 文件中转译的导入可以成为具有特殊 ID import "foo?require=true"
的常规导入,以便解析器插件知道这一点。
然而,这里的问题是,此代理 ID 可能导致或可能不会导致传递给其他解析器的意外副作用,因为它并不真正对应于文件。此外,如果 ID 是由插件 A
创建的,而解析发生在插件 B
中,则会在这些插件之间创建一个依赖关系,因此 A
无法在没有 B
的情况下使用。
自定义解析器选项通过允许在通过 this resolve
手动解析模块时为插件传递其他选项来提供解决方案。这在不更改 ID 的情况下发生,因此不会影响其他插件在目标插件不存在的情况下正确解析模块的能力。
function requestingPlugin() {
return {
name: 'requesting',
async buildStart() {
const resolution = await this.resolve('foo', undefined, {
custom: { resolving: { specialResolution: true } }
});
console.log(resolution.id); // "special"
}
};
}
function resolvingPlugin() {
return {
name: 'resolving',
resolveId(id, importer, { custom }) {
if (custom.resolving?.specialResolution) {
return 'special';
}
return null;
}
};
}
请注意,自定义选项应使用与解析插件的插件名称相对应的属性添加。解析插件有责任指定它尊重哪些选项。
自定义模块元数据
插件可以使用自定义元数据来注释模块,这些元数据可以由它们自己和其他插件通过 resolveId
、load
和 transform
钩子设置,并通过 this.getModuleInfo
、this.load
和 moduleParsed
钩子访问。此元数据应始终是 JSON.stringifyable 的,并且将在缓存中持久化,例如在监视模式下。
function annotatingPlugin() {
return {
name: 'annotating',
transform(code, id) {
if (thisModuleIsSpecial(code, id)) {
return { meta: { annotating: { special: true } } };
}
}
};
}
function readingPlugin() {
let parentApi;
return {
name: 'reading',
buildEnd() {
const specialModules = Array.from(this.getModuleIds()).filter(
id => this.getModuleInfo(id).meta.annotating?.special
);
// do something with this list
}
};
}
请注意,添加或修改数据的插件应使用与插件名称相对应的属性,在本例中为 annotating
。另一方面,任何插件都可以通过 this.getModuleInfo
读取来自其他插件的所有元数据。
如果多个插件添加元数据或元数据是在不同的钩子中添加的,那么这些 meta
对象将被浅层合并。这意味着如果插件 first
在 resolveId 钩子中添加 {meta: {first: {resolved: "first"}}}
并在 load 钩子中添加 {meta: {first: {loaded: "first"}}}
,而插件 second
在 transform
钩子中添加 {meta: {second: {transformed: "second"}}}
,那么生成的 meta
对象将是 {first: {loaded: "first"}, second: {transformed: "second"}}
。在这里,resolveId
钩子的结果将被 load
钩子的结果覆盖,因为插件同时将它们存储在其 first
顶级属性下。另一方面,另一个插件的 transform
数据将放置在其旁边。
模块的 meta
对象在 Rollup 开始加载模块时创建,并在模块的每个生命周期钩子中更新。如果您存储对该对象的引用,您也可以手动更新它。要访问尚未加载的模块的元对象,您可以通过 this.load
触发其创建和加载模块
function plugin() {
return {
name: 'test',
buildStart() {
// trigger loading a module. We could also pass an initial
// "meta" object here, but it would be ignored if the module
// was already loaded via other means
this.load({ id: 'my-id' });
// the module info is now available, we do not need to await
// this.load
const meta = this.getModuleInfo('my-id').meta;
// we can also modify meta manually now
meta.test = { some: 'data' };
}
};
}
直接插件通信
对于任何其他类型的插件间通信,我们建议使用以下模式。请注意,api
永远不会与任何即将推出的插件钩子冲突。
function parentPlugin() {
return {
name: 'parent',
api: {
//...methods and properties exposed for other plugins
doSomething(...args) {
// do something interesting
}
}
// ...plugin hooks
};
}
function dependentPlugin() {
let parentApi;
return {
name: 'dependent',
buildStart({ plugins }) {
const parentName = 'parent';
const parentPlugin = plugins.find(
plugin => plugin.name === parentName
);
if (!parentPlugin) {
// or handle this silently if it is optional
throw new Error(
`This plugin depends on the "${parentName}" plugin.`
);
}
// now you can access the API methods in subsequent hooks
parentApi = parentPlugin.api;
},
transform(code, id) {
if (thereIsAReasonToDoSomething(id)) {
parentApi.doSomething(id);
}
}
};
}