banli17 / note

学习记录
https://banli17.github.io/note/docs/intro
MIT License
17 stars 2 forks source link

rollup 的构建钩子问题 #79

Open banli17 opened 2 years ago

banli17 commented 2 years ago
  1. rollup 构建钩子有哪些?
  2. 构建钩子的种类,介绍每种类型?
  3. 画出构建钩子的生命周期,并用例子分析?
  4. 说说钩子的执行顺序?
  5. sequential 属性是干什么的,以及它的适用范围?
  6. 构建钩子的参数和返回值?

https://rollupjs.org/guide/en/#build-hooks

banli17 commented 2 years ago

构建钩子的种类,介绍每种类型

为了影响构建过程,插件对象需要包含钩子,通过钩子,钩子函数在构建的一系列阶段调用。甚至可以修改完整的构建过程。

构建钩子有4类:

banli17 commented 2 years ago

钩子可以是函数,也可以是对象,如果是对象,需要指定属性 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);
        }
      }
    };
  }
banli17 commented 2 years ago

构建阶段,其实是执行了 rollup.rollup(inputOptions) 。主要是处理 输入文件定位、加载和转换输入文件。构建阶段的第一个钩子是 options,最后一个总是 buildEnd. 如果有构建错误,closeBundle将在此之后调用。

在 watch 模式下,watchChange 在需要重新构建时触发。当观察者关闭时,closeWatcher 将触发。

image
banli17 commented 2 years ago

每个钩子函数详情

buildEnd 类型: (error?: Error) => void 种类: async, parallel 上一个钩子: moduleParsedresolveIdresolveDynamicImport。 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,然后再关闭进程。这个钩子不能用于输出文件。

banli17 commented 2 years ago

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: resolveIdresolveDynamicImport 另外,这个钩子会在调用 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 如果返回值 是

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 上下文.

banli17 commented 2 years ago
// 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;
    }
  };
}