Make writing a habit together! This is the sixth day of my participation in the “Gold Digging Day New Plan · April More text Challenge”. Click here for more details.

Hello everyone, I am Xiao Yu. In the previous section, we learned about ModuleGraph, which uses the pluginContainer API when parsing, loading, and converting modules. But what are the capabilities of pluginContainer? How does a Vite plugin relate to a Rollup plugin? This article will reveal the answers to these questions.

As usual, let’s start with a minimal plugin DEMO:

// vite.config.ts
import { defineConfig } from 'vite'
import { VitePluginBaz } from './plugins/vite-plugin-baz'

export default defineConfig({
  plugins: [
    VitePluginBaz(),
    
    {
      name: 'foo',

      buildStart (ctx) {
        console.log('foo')}},async() = > {return {
        name: 'bar',

        buildStart (ctx) {
          console.log(ctx.name)
          console.log('bar plugin'}}}]})// ./plugins/vite-plugin-baz
import { Plugin } from 'vite'

export const VitePluginBaz = (): Plugin= > {
  return {
    name: 'baz',

    buildStart (ctx) {
      console.log('baz')}}}Copy the code

We used three plugins — foo, bar and baz — in vite.config.ts for the above code. Foo and bar are defined directly in vite.config.ts, and baz are imported from external files, and both define buildStart hooks.

// Receive the incoming configuration to create the service
export async function createServer(
  inlineConfig: InlineConfig = {}
) :Promise<ViteDevServer> {
  // Get the config for development or server from CLI + default parameters
  const config = await resolveConfig(inlineConfig, 'serve'.'development')

  // ...
  const watcher = chokidar.watch(path.resolve(root), {
    // ...
  }) as FSWatcher
  
  // Initialize the module map
  const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) = >
    container.resolveId(url, undefined, { ssr })
  )
  
  // Create a plug-in container
  const container = await createPluginContainer(config, moduleGraph, watcher)

  // ...
  
  if(! middlewareMode && httpServer) { httpServer.listen = (async (port: number. args:any[]) = > {if(! isOptimized) {try {
          // The plug-in container is initialized
          await container.buildStart({})
          // ...
        }
        // ...
      }
      returnlisten(port, ... args) })as any
  }

  return server
}
Copy the code

Following resolveConfig, chokidar. Watch instantiates a file monitor, instantiates a ModuleGraph from the ModuleGraph class, Now you see the heart of this section — creating the plug-in container with createPluginContainer, passing in the entire config, the moduleGraph, and the file monitoring instance watcher.

Here are screenshots of the three parameters in the example:

Config: resolveConfig

ModuleGraph: An instance of moduleGraph:

Watcher: an instance of the current directory compatible with chokidar:

With the above three parameters, we can create the plug-in container by calling createPluginContainer:

/** * Create plug-in container *@param Config Parsed configuration *@param The moduleGraph module depends on the object *@param The watcher file listens for instances *@returns The container object */
export async function createPluginContainer({ plugins, logger, root, build: { rollupOptions } }: ResolvedConfig, moduleGraph? : ModuleGraph, watcher? : FSWatcher) :Promise<PluginContainer> {
  // ...
  // Listen for an array of files
  const watchFiles = new Set<string> ()// Get the rollup version
  const rollupPkgPath = resolve(require.resolve('rollup'), '.. /.. /package.json')

  // Minimal context information
  const minimalContext: MinimalPluginContext = {
    meta: {
      rollupVersion: JSON.parse(fs.readFileSync(rollupPkgPath, 'utf-8'))
        .version,
      watchMode: true}}// An incompatible vite plug-in alarm function was used
  function warnIncompatibleMethod(method: string, plugin: string) {
    // ...
  }

  const ModuleInfoProxy: ProxyHandler<ModuleInfo> = {
    // ...
  }

  // same default value of "moduleInfo.meta" as in Rollup
  const EMPTY_OBJECT = Object.freeze({})

  function getModuleInfo(id: string) {
    // ...
  }

  function updateModuleInfo(id: string, { meta }: { meta? :object | null }) {
    // ...
  }

  // Plugin context plugin that implements the rollup plugin interface
  class Context implements PluginContext {
    meta = minimalContext.meta
    ssr = false
    _activePlugin: Plugin | null
    _activeId: string | null = null
    _activeCode: string | null = null_resolveSkips? :Set<Plugin>
    _addedImports: Set<string> | null = null

    constructor(initialPlugin? : Plugin) {
      this._activePlugin = initialPlugin || null
    }

    /** * compile code */
    parse(code: string, opts: any = {}) {
      // ...
    }

    async resolve(
      id: string, importer? :string, options? : { skipSelf? :boolean }
    ) {
      // ...
    }

    getModuleInfo(id: string) {
      return getModuleInfo(id)
    }

    getModuleIds() {
      return moduleGraph
        ? moduleGraph.idToModuleMap.keys()
        : Array.prototype[Symbol.iterator]()
    }

    /** * Add hot monitor file */
    addWatchFile(id: string){ watchFiles.add(id) ; (this._addedImports || (this._addedImports = new Set())).add(id)
      if (watcher) ensureWatchedFile(watcher, id, root)
    }

    /** * get all the hot files */
    getWatchFiles() {
      return [...watchFiles]
    }

    emitFile(assetOrFile: EmittedFile) {
      warnIncompatibleMethod(`emitFile`.this._activePlugin! .name)return ' '
    }

    setAssetSource() {
      warnIncompatibleMethod(`setAssetSource`.this._activePlugin! .name) }getFileName() {
      warnIncompatibleMethod(`getFileName`.this._activePlugin! .name)return ' '
    }

    warn(
      e: string| RollupError, position? :number | { column: number; line: number }
    ) {
      // ...
    }

    error(
      e: string| RollupError, position? :number | { column: number; line: number }
    ): never (
      throw formatError(e, position, this)}}function formatError(
    e: string | RollupError,
    position: number | { column: number; line: number } | undefined,
    ctx: Context
  ) {
    // ...
  }

	// File compile context plug-in
  class TransformContext extends Context {
    // ...
  }

  let closed = false

  // Define plug-in container -> rollup build hook
  const container: PluginContainer = {
    options: await (async() = > {// ...
    })(),

    getModuleInfo,

    async buildStart() {
      // ...
    },

    async resolveId(rawId, importer = join(root, 'index.html'), options) {
      // ...
    },

    async load(id, options) {
      // ...
    },

    async transform(code, id, options) {
      // ...
    },

    async close() {
      // ...}}return container
}
Copy the code

TransformContext is a subclass of Context, and Context implements the PluginContext interface. PluginContext is imported from the rollup package. So the Vite plugin is basically the same as the Rollup plugin, but not completely compatible.

WarnIncompatibleMethod generates alarms for incompatible methods. Methods such as emitFile, setAssetSource, and getFileName cannot be used in the context of Vite plug-ins. If you do not want to use them, You will also get the corresponding warning. Let’s try calling getFileName from the viet-plugin-baz plugin:

// ./plugins/vite-plugin-baz
import { Plugin } from 'vite'

export const VitePluginBaz = (): Plugin= > {
  return {
    name: 'baz',

    buildStart (ctx) {
      console.log('baz')
      const filename = this.getFileName()
      console.log(filename)
    }
  }
}
Copy the code

After executing dev, we get the following warning:

The relationship between the Container API and MinimalPluginContext, Context, and TransformContext can be summarized in the following figure:

PluginContainer exposes options, getModuleInfo, buildStart, resolveId, Load, close, Transform, and other hooks we are familiar with when writing Vite plugins; In addition to these rollup hooks, we also know config and configResolved hooks in Vite’s technical secrets, Know about transformIndexHtml and configureServer hooks in Vue Tech Reveal CreateServer, and in subsequent HMR unveiling, Learn about its technical principles (part 1) and come into contact with the hot timer hook handleHotUpdate.

The implementation of these hooks is largely dependent on the implementation of the rollup plugin hook. Let’s analyze the hook source code to see how Vite leverages the rollup capability.

options

const minimalContext: MinimalPluginContext = {
  meta: {
    rollupVersion: JSON.parse(fs.readFileSync(rollupPkgPath, 'utf-8'))
    .version,
    watchMode: true}}// ...

options: await (async() = > {// User customizes Rollup configuration from build.rollupOptions
  let options = rollupOptions
  // Call the plug-in's options method in the minimalContext
  for (const plugin of plugins) {
    if(! plugin.options)continue
    options =
      (await plugin.options.call(minimalContext, options)) || options
  }
  // https://rollupjs.org/guide/en/#acorninjectplugins
  // Can configure plug-ins for the underlying compiler of rollup
  if (options.acornInjectPlugins) {
    parser = acorn.Parser.extend(options.acornInjectPlugins as any)}return {
    acorn,
    acornInjectPlugins: [],
    ...options
  }
})(),
Copy the code

Options is an asynchronous instant-execute function that fetches the rollupOptions configuration item from build.rollupOptions and calls the plug-in’s Options hook as an argument. The execution plug-in hook function context is minimalContext, whose meta attribute comes from the PluginContextMeta type in rollup. You can also inject plug-ins into the underlying Acorn compiler via acornInjectPlugins. Finally returns the compiler, the list of Acorn plug-ins, and finally the parameter passed to the rollup.rollup option.

getModuleInfo

const ModuleInfoProxy: ProxyHandler<ModuleInfo> = {
  get(info: any, key: string) {
    if (key in info) {
      return info[key]
    }
    throw Error(
      `[vite] The "${key}" property of ModuleInfo is not supported.`)}}// same default value of "moduleInfo.meta" as in Rollup
const EMPTY_OBJECT = Object.freeze({})

function getModuleInfo(id: string) {
  // Get the module by id
  const module= moduleGraph? .getModuleById(id)if (!module) {
    return null
  }
  // Module. info comes from ModuleInfo in rollup
  if (!module.info) {
    // If it does not exist, use Proxy to give a friendly prompt message
    module.info = new Proxy(
      { id, meta: module.meta || EMPTY_OBJECT } as ModuleInfo,
      ModuleInfoProxy
    )
  }
  return module.info
}
Copy the code

Through moduleGraph getModuleInfo hook. The complete module to obtain getModuleById function. If you don’t have access to the corresponding module. The info, will be through the agent {id, meta: module. Meta | | EMPTY_OBJECT} object returns the info attribute. The purpose of the proxy is to give an Error message when retrieving nonexistent properties.

buildStart

async buildStart() {
  await Promise.all(
    // Recursively calls the plug-in's buildStart hook, which can be the promise function
    // The Context is the rollup plug-in Context instance with the argument
    plugins.map((plugin) = > {
      if (plugin.buildStart) {
        return plugin.buildStart.call(
          new Context(plugin) as any,
          container.options as NormalizedInputOptions
        )
      }
    })
  )
},
Copy the code

The buildStart hook is called before the service starts, and the hook logic is clear. Each plug-in’s buildStart hook is loiterably called, and the execution Context is an instance of the Context and the argument is the return value of the immediate execution function container.options.

resovleId

/** * Parsing module ID *@param {string} RawId id in code *@param {string} Importer, the default is the root path of index.html *@param {PluginContainer.resolveId}* /
async resolveId(rawId, importer = join(root, 'index.html'), options) {
  constskip = options? .skipconstssr = options? .ssr// Create a function execution Context, as shown in the preceding figure. Context is a PluginContext that Vite inherits from rollup
  const ctx = newContext() ctx.ssr = !! ssr ctx._resolveSkips = skiplet id: string | null = null
  const partial: Partial<PartialResolvedId> = {}
  // Loop through the plug-in's resolveId hook
  for (const plugin of plugins) {
    // The resolveId hook function is undefined
    if(! plugin.resolveId)continue
    / / to skip
    if(skip? .has(plugin))continue

    ctx._activePlugin = plugin

    // Execute the resolveId function of the plug-in
    const result = await plugin.resolveId.call(
      ctx as any,
      rawId,
      importer,
      { ssr }
    )
    if(! result)continue

    // Process the return value
    if (typeof result === 'string') {
      id = result
    } else {
      id = result.id
      Object.assign(partial, result)
    }

    // resolveId() is hookFirst - first non-null result is returned.
    break
  }

  // ...

  if (id) {
    partial.id = isExternalUrl(id) ? id : normalizePath(id)
    return partial as PartialResolvedId
  } else {
    return null}}Copy the code

Resolveids are very important and can be found throughout the Vite process. The resolution of the ID can be seen in any pre-build, CSS, request compilation, transformation process, etc., all by calling the resolveId function of the plug-in. As can be seen from the above code, the Context in which resolveId is executed is also an instance of Context. Parameters are the ID of the corresponding module and the object importer which references this module. If the id returned is an external link, it will be directly returned. Otherwise, the path is normalized and the absolute path is output.

load

function updateModuleInfo(id: string, { meta }: { meta? :object | null }) {
  if (meta) {
    const moduleInfo = getModuleInfo(id)
    if(moduleInfo) { moduleInfo.meta = { ... moduleInfo.meta, ... meta } } } }async load(id, options) {
  constssr = options? .ssrconst ctx = newContext() ctx.ssr = !! ssrfor (const plugin of plugins) {
    if(! plugin.load)continue
    ctx._activePlugin = plugin
    const result = await plugin.load.call(ctx as any, id, { ssr })
    if(result ! =null) {
      if (isObject(result)) {
        updateModuleInfo(id, result)
      }
      return result
    }
  }
  return null
}
Copy the code

In ModuleGraph, a Vite technical debunker, we talked about how the load hook is triggered to retrieve the module code and map when the module is compiled and transformed. The load hook is also very simple. You get the module ID and call the load hooks of all the plug-ins in sequence, and the execution Context is still the Context instance. Update the module’s meta attribute if the return value is not empty and is an object. Any custom attributes of a module can be stored by returning the meta field in the Load hook.

close

 async close() {
   // It is already closed, so there is no need to handle anything
   if (closed) return
   const ctx = new Context()
   // Loop through the plugin's buildEnd hook
   await Promise.all(
     plugins.map((p) = > p.buildEnd && p.buildEnd.call(ctx as any)))// Call the closeBundle hook of the plug-in
   await Promise.all(
     plugins.map((p) = > p.closeBundle && p.closeBundle.call(ctx as any))
   )
   closed = true
 }
Copy the code

Vite wraps both the Rollup buildEnd and closeBundle hooks in the close function. BuildEnd and closeBundle are executed in the Context of the Context instance and use rollup’s plug-in capabilities.

transform

async transform(code, id, options) {
 	// ...
  // Create a transformation context using TransformContext
  const ctx = new TransformContext(id, code, inMap as SourceMap)
  
  for (const plugin of plugins) {
    if(! plugin.transform)continue
   
    // ...
    let result: TransformResult | string | undefined
    try {
      result = await plugin.transform.call(ctx as any, code, id, { ssr })
    } catch (e) {
      ctx.error(e)
    }
    if(! result)continue
    // ...
    if (isObject(result)) {
      if(result.code ! = =undefined) {
        code = result.code
        if (result.map) {
          ctx.sourcemapChain.push(result.map)
        }
      }
      updateModuleInfo(id, result)
    } else {
      code = result
    }
  }
  return {
    code,
    map: ctx._getCombinedSourcemap()
  }
}
Copy the code

The transform hook is used to do the final translation of the code returned by the Load hook. Unlike the hooks above, the Transform hook function executes in an instance of TransformContext. TransformContext inherits Context and has more about the processing power of Sourcemap. Transform gets result, pushes result.map to sourcemapChain, and then updates the meta property of the module just as the load hook does, returning code and map.

conclusion

In the createServer main flow, we know that the plug-in container is created after the configuration is parsed, the moduleGraph is created, and the file listener is created.

Inside createPluginContainer, three Vite contexts are defined: MinimalPluginContext, Context, and TransformContext. MinimalPluginContext reuses rollup directly. Context reuses most of the capabilities of rollup’s PluginContext, but there are incompatibable situations. EmitFile, setAssetSource, and getFileName are not used in Vite.

Then we know that the plug-in container manages config.plugins and calls hook functions in turn when the corresponding hooks are executed. The function execution context is based on rollup, which completes the reuse of capabilities. Now go back to the plug-in diagram for Vite and Rollup:

Is it more specific?