Vite plugin mechanism

Naming conventions

If the plug-in does not use Vite specific hooks, it can be implemented as a compatible Rollup plug-in. The Rollup plug-in name convention is recommended.

  • The Rollup plug-in should have a striprollup-plugin-A name with a prefix and clear meaning.
  • Included in package.jsonrollup-pluginvite-pluginThe keyword.

Thus, plug-ins can also be used for pure Rollup or WMR-based projects.

For Vite specific plug-ins:

  • The Vite plugin should have a strapvite-plugin-A name with a prefix and clear meaning.
  • Included in package.jsonvite-pluginThe keyword.
  • Add a section to the plug-in documentation explaining why this plug-in is a Vite-specific plug-in (for example, this plug-in uses vite-specific plug-in hooks).

If your plugin only works with a specific framework, its name should follow the following prefix format:

  • vite-plugin-vue-Prefix as Vue plug-in
  • vite-plugin-react-Prefix as React plugin
  • vite-plugin-svelte-Prefix as Svelte plugin

The plug-in configuration

The user adds plug-ins to the project’s devDependencies and configures them using the plugins option in the form of an array.

import vitePlugin from 'vite-plugin-feature'
import rollupPlugin from 'rollup-plugin-feature'
export default {
  plugins: [vitePlugin(), rollupPlugin()]
}
Copy the code

Plug-ins with false values are ignored and can be used to easily enable or disable plug-ins.

Plugins can also accept multiple plug-ins as a default for a single element. This is useful for complex features such as framework integration that are implemented using multiple plug-ins. The array will be flatten internally.

// Frame plugin
import frameworkRefresh from 'vite-plugin-framework-refresh'
import frameworkDevtools from 'vite-plugin-framework-devtools'
export default function framework(config) {
  return [frameworkRefresh(config), frameworkDevTools(config)]
}
Copy the code
// vite.config.js
import framework from 'vite-plugin-framework'
export default {
  plugins: [framework()]
}
Copy the code

A simple example

Import a virtual file
export default function myPlugin() {
  const virtualFileId = '@my-virtual-file'
  return {
    name: 'my-plugin'.// Yes, will be displayed in warning and error
    resolveId(id) {
      if (id === virtualFileId) {
        return virtualFileId
      }
    },
    load(id) {
      if (id === virtualFileId) {
        return `export const msg = "from virtual file"` // Replace the file contents}}}}Copy the code

This makes it possible to introduce these files in JavaScript:

import { msg } from '@my-virtual-file'
console.log(msg) // from virtual file
Copy the code
Convert custom file types
const fileRegex = /\.(my-file-ext)$/
export default function myPlugin() {
  return {
    name: 'transform-file'.transform(src, id) {
      if (fileRegex.test(id)) {
        return {
          code: compileFileToJS(src), // File type conversion
          map: null // Source map will be provided if feasible
        }
      }
    }
  }
}
Copy the code

General hook

During development, the Vite development server creates a plug-in container to invoke the Rollup build hook, just like Rollup.

The following hooks are called when the server starts
  • options
  • buildStart
options
  • Type: ( inputOptions ) => options

The first hook that rollup packages, used to replace or manipulate rollup configuration until rollup is fully configured, returns null, indicating that nothing will be done. If it is simply to read the rollup configuration file, it can be obtained in the buildStart hook. Also, it is the only hook in rollup that cannot retrieve the Plugin context, and this hook should be rarely used.

buildStart
  • Type: (options: InputOptions) => void
  • Previous Hook: options
  • Next Hook: resolveId

This hook follows the Options hook and is fired at rollup build time to get rollup configuration

The following hooks are called on each module request
resolveId
  • Type: (importee, importer) => (id|Promise)
  • Previous Hook: buildStart, moduleParsed, resolveDynamicImport.
  • Next Hook: load

If buildStart, moduleParsed, and resolveDynamicImport are configured, the resolveId hook will fire after the first three hooks. It should be noted that the moduleParsed and resolveDynamicImport hooks are not used in serve(development mode) rollup. Resolve triggers the resolveId hook whenever a plugin triggers this.emitFile or this.resolve manually resolves an ID; Returns NULL, indicating that the default parsing method is used. Returns false to indicate that Importee is an external module and will not be packaged into the bundle.

async resolveId(importee,importer) {
  // Importee indicates the chunk itself, the importer indicates the chunk that importee is introduced
  Importee = @my-virtual-file, importer = the absolute path to app.tsx
  if(! importer) {// We need to skip this plugin to avoid an infinite loop
    const resolution = await this.resolve(importee, undefined, { skipSelf: true });
    // If it cannot be resolved, return `null` so that Rollup displays an error
    if(! resolution)return null;
    return `${resolution.id}? entry-proxy`;
  }
  return null;
},
load(id) {
  if (id.endsWith('? entry-proxy')) {
    // get resolution.id
    const importee = id.slice(0, -'? entry-proxy'.length);
    // Note that this will throw if there is no default export
    return `export {default} from '${importee}'; `;
  }
  return null;
}
Copy the code
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}
  • Previous Hook: resolveId or resolveDynamicImport
  • Next Hook: transform

Custom loader to return custom file contents; If null is returned, the default content of the chunk parsed by the system is returned. Load can return many types of content, including sourceMap and AST. For details, see Load

transform
  • Type: (code: string, 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}
  • Previous Hook: load
  • NextHook: moduleParsed once the file has been processed and parsed.

It is used to convert the chunk after load to avoid extra compilation overhead

The following hooks are called when the server is shut down
  • buildEnd
  • closeBundle
buildEnd
  • Type: (error? : Error) => void
  • Previous Hook: moduleParsed, resolveId or resolveDynamicImport.
  • Next Hook: outputOptions in the output generation phase as this is the last hook of the build phase.

This can be triggered when bunding finished, before writing a file, or it can return a Promise. This hook is also triggered if an error is reported during build

Vite unique hook

config
  • Type: (config: UserConfig, env: { mode: string, command: string }) => UserConfig | null | void

Called before parsing the Vite configuration. The hook receives the original user configuration (which is merged with the configuration file as specified by the command line option) and a variable that describes the configuration environment, including the mode and command being used. It can return a partial configuration object that will be deeply merged into an existing configuration, or simply change the configuration if the default merge does not achieve the desired result. Call anything else inside the Config hook

// Return to partial configuration (recommended)
const partialConfigPlugin = () = > ({
  name: 'return-partial'.config: () = > ({
    alias: {
      foo: 'bar'}})})// Change configuration directly (should only be used if merge does not work)
const mutateConfigPlugin = () = > ({
  name: 'mutate-config'.config(config, { command }) {
    if (command === 'build') {
      config.root = __dirname
    }
  }
})
Copy the code
configResolved
  • Type: (config: ResolvedConfig) => void | Promise<void>

Called after the Vite configuration is parsed. Use this hook to read and store the final parsed configuration. It is also useful when the plug-in needs to do something different depending on the command being run.

const exmaplePlugin = () = > {
  let config
  return {
    name: 'read-config'.configResolved(resolvedConfig) {
      // Stores the final parsed configuration
      config = resolvedConfig
    },
    // Use the configuration stored with other hooks
    transform(code, id) {
      if (config.command === 'serve') {
        // Serve: a plug-in used to start the development server
      } else {
        // build: calls the Rollup plug-in}}}}Copy the code
configureServer
  • Type: (server: ViteDevServer) => (() => void) | void | Promise<(() => void) | void>
  • ViteDevServer Interface: ViteDevServer

Is the hook used to configure the development server. The most common use case is to add custom middleware to an internal CONNECT application:

const myPlugin = () = > ({
  name: 'configure-server'.configureServer(server) {
    server.middlewares.use((req, res, next) = > {
      // Custom request handling...})}})Copy the code

Inject the back-end middleware

The configureServer hook will be called before the internal middleware is installed, so the custom middleware will run before the internal middleware by default. If you want to inject a middleware running behind the internal middleware, you can return a function from configureServer that will be called after the internal middleware is installed:

const myPlugin = () = > ({
  name: 'configure-server'.configureServer(server) {
    // Returns an internal middleware after installation
    // The post-hook to be called
    return () = > {
      server.middlewares.use((req, res, next) = > {
        // Custom request handling...}}}})Copy the code

Note that configureServer is not called when running the production version, so other hooks need to be careful not to miss it.

transformIndexHtml
  • Type: IndexHtmlTransformHook | { enforce? : 'pre' | 'post' transform: IndexHtmlTransformHook }

Convert the special hook for index.html. The hook receives the current HTML string and the conversion context. The context exposes the ViteDevServer instance during development and the Rollup output package during build.

This hook can be asynchronous and can return one of the following:

  • Converted HTML string
  • An array of tag descriptor objects injected into existing HTML ({ tag, attrs, children }). Each tag can also specify where it should be injected (default is<head>Before)
  • A contain{ html, tags }The object of
const htmlPlugin = () = > {
  return {
    name: 'html-transform'.transformIndexHtml(html) {
      return html.replace(
        /(.*?) <\/title>/.`Title replaced! `)}}}Copy the code
handleHotUpdate
  • Type: (ctx: HmrContext) => Array<ModuleNode> | void | Promise<Array<ModuleNode> | void>
interface HmrContext {
  file: string
  timestamp: number
  modules: Array<ModuleNode>
  read: () => string | Promise<string>
  server: ViteDevServer
}
Copy the code
  • modulesIs an array of modules affected by the change file. It is an array because a single file may map to multiple service modules (such as a Vue single-file component).
  • readThis is an asynchronous read function that returns the contents of the file. This is done because on some systems, the callback function for file changes may fire too quickly before the editor has finished updating the file, andfs.readFileEmpty content is returned. The incomingreadFunctions regulate this behavior.

Hooks can be selected:

  • Filter and narrow the list of affected modules to make HMR more accurate.
  • Returns an empty array and performs full custom HMR processing by sending custom events to the client:
handleHotUpdate({ server }) {
  server.ws.send({
    type: 'custom'.event: 'special-update'.data: {}})return[]}Copy the code

Client code should register the corresponding handler using the HMR API (this should be injected by the transform hook of the same plug-in) :

if (import.meta.hot) {
  import.meta.hot.on('special-update'.(data) = > {
    // Perform custom updates})}Copy the code

Summary of hook execution order

export default function myExample () {
    // The plugin object is returned
    return {
        name: 'hooks-order'.// Initialize hooks, only once
        options(opts) {
            console.log('options');
        },
        buildStart() {
            console.log('buildStart');
        },
        // Vite has unique hooks
        config(config) {
            console.log('config');
            return{}},configResolved(resolvedCofnig) {
            console.log('configResolved');
        },
        configureServer(server) {
            console.log('configureServer');
            // server.app.use((req, res, next) => {
            // // custom handle request...
            // })
        },
        transformIndexHtml(html) {
            console.log('transformIndexHtml');
            return html
            // return html.replace(
            // /(.*?) <\/title>/,
            // `Title replaced! `
            // )
        },
        // Generic hook
        resolveId(source) {
            console.log(resolveId)
            if (source === 'virtual-module') {
                console.log('resolvedId');
                return source; 
            }
            return null; 
        },
        load(id) {
            console.log('load');
                
            if (id === 'virtual-module') {
                return 'export default "This is virtual!" ';
            }
            return null;
        },
        transform(code, id) {
            console.log('transform');
            if (id === 'virtual-module') {}return code
        },
    };
  }

Copy the code

The execution result

config
configResolved
options
configureServer
buildStart
transformIndexHtml
load
load
transform
transform
Copy the code

Hook execution order

Order of plug-in execution

Similar to WebPack, this is controlled by the Enforce field

  • Alias handling Alias
  • User plug-in Settingsenforce: 'pre'
  • Vite core plug-in
  • The user plug-in is not configuredenforce
  • Vite builds plug-ins
  • User plug-in Settingsenforce: 'post'
  • Vite build post-plugins (Minify, Manifest, Reporting)

Since vue3+ Vite will be used in the subsequent architecture upgrade of the company, considering that some wheels of Vite may not be perfect for the time being, it is not ruled out that we need to write vite plug-in for the subsequent work, so I will make a summary here, and I hope to correct any mistakes.

Reference links:

Juejin. Cn/post / 695021…

Cn. Vitejs. Dev/guide/API – p…

Rollupjs.org/guide/en/#p…