preface

Last article Vite development practice – project construction we talked about the Vite project environment construction, mainly to supplement the Vite scaffolding construction project defects. The specific development part of the official has given us a good experience and examples, some of the more special uses such as glob directory scanning, static resource import, it is recommended to directly read the official documents. This article will focus on how to write plug-ins during development to improve the development experience.

Plugin mechanism in Vite

Here’s an official quote:

The Vite plug-in extends the well-designed Rollup interface with some configuration items unique to Vite. Therefore, you only need to write a Vite plug-in to work in both development and production environments.

The vite plugin is based on the rollup extension. The plugin mechanism can be used to extend the application. Here is a brief introduction to the plugin mechanism in Vite.

The plug-in configuration

Using plug-ins in Vite is as simple as declaring them in the plugins option in vite.config.ts.

import vitePlugin from 'vite-plugin-feature'
import rollupPlugin from 'rollup-plugin-feature'

export default defineConfig({
  plugins: [vitePlugin(), rollupPlugin()]
})
Copy the code

Each Plugin function needs to return a Plugin object with the name field, which can contain some plug-in hooks that Vite will call at execution time.

And false values for plug-ins are automatically ignored by Vite, so optional plug-ins don’t need to filter the set of available plug-ins as webPack does.

The plugin hooks

We discussed the concept of plug-in hooks in Vite. Now let’s talk about what hooks are available in Vite.

Rollup compatible hooks

Rollup () is not compatible with all of vite’s rollup hooks. Only the following hooks are called, so it can be assumed that vite’s development server only calls rollup.rollup() and not bundle.generate().

  • The following hooks are called when the server starts:

    • Options: Replaces or manipulates the option object passed to rollup. Nothing is replaced when null is returned. And this is the only hook that doesn’t have access to most plug-in context utility functionality because it runs before rollup is fully configured.

      Note: If we only want to access the option object passed to rollup, we recommend using buildStart below.

      Type definition:

        interface PluginHooks {
            options: (
                        this: MinimalPluginContext,
                        options: InputOptions
                ) = > Promise<InputOptions | null | undefined> | InputOptions | null | undefined;
        }
      Copy the code
    • BuildStart: called at every rollup build. This is the recommended hook to use when you need to access options passed to rollup, because it takes into account the conversion of all option hooks and also contains correct defaults for unset options.

      Type definition:

        interface PluginHooks {
            buildStart: (this: PluginContext, options: NormalizedInputOptions) = > Promise<void> | void;
        }
      Copy the code
  • The following hooks are called on each incoming module request:

    • ResolveId: can be used to define custom ID path resolvers, such as./foo.js in import foo from ‘./foo.js’, generally used to locate third party dependencies.

      Type definition:

      interface Plugin {
          Rollup resolveId is extended to add SSR flagresolveId? (this: PluginContext, source: string.importer: string | undefined.options:{ custom? : CustomPluginOptions; }, ssr? :boolean) :Promise<ResolveIdResult> | ResolveIdResult;
      }
      Copy the code
    • Load: Can be used to define a custom module parsing loader, for example, can directly return a converted AST.

      Type definition:

      interface Plugin {
         Rollup resolveId is extended to add SSR flagload? (this: PluginContext, id: string, ssr? :boolean) :Promise<LoadResult> | LoadResult;
      }
      Copy the code
    • Transform can also return the AST, but at this point the code of the path file is already in hand, so it is usually used to transform the loaded module. For example, markdown parsing is based on this hook.

      Type definition:

      interface Plugin {
         Rollup resolveId is extended to add SSR flagtransform? (this: TransformPluginContext, code: string.id: string, ssr? :boolean) :Promise<TransformResult_2> | TransformResult_2;
      }
      Copy the code
  • The following hooks are called when the server is shut down:

    • BuildEnd: called when the build is complete to get an error message that the build failed.

      Type definition:

      interface PluginHooks {
         buildEnd: (this: PluginContext, err? :Error) = > Promise<void> | void;
      }
      Copy the code
    • CloseBundle: Any external services that may be running can be cleaned up during this period.

      Type definition:

      interface PluginHooks {
      closeBundle: (this: PluginContext) = > Promise<void> | void;
      }
      Copy the code

Vite unique hooks

Vite’s unique hooks are ignored by Rollup

  • Config: called before parsing the vite configuration, where you can extend or modify the original user definition directly.

    Type definition:

    interfacePlugin { config? :(config: UserConfig, env: ConfigEnv) = > UserConfig | null | void | Promise<UserConfig | null | void>;
    }
    Copy the code
  • ConfigResolved: Called after the Vite configuration is resolved. Use this hook to read and store the final parsed configuration.

    Type definition:

    interfacePlugin { configResolved? :(config: ResolvedConfig) = > void | Promise<void>;
    }
    Copy the code
  • ConfigureServer: Hook used to configure the development server, access the development server instance from the hook, and can be used to add custom middleware to intercept requests, such as the mock plug-in based on this hook extension.

    Type definition:

    export declare type ServerHook = (server: ViteDevServer) = > (() = > void) | void | Promise< (() = > void) | void>;
    interfacePlugin { configureServer? : ServerHook; }Copy the code
  • TransformIndexHtml: Special hook for converting index.html. Type definition:

    export declare type IndexHtmlTransformHook = (html: string, ctx: IndexHtmlTransformContext) = > IndexHtmlTransformResult | void | Promise<IndexHtmlTransformResult | void>;
    interfacePlugin { transformIndexHtml? : IndexHtmlTransform; }Copy the code
  • HandleHotUpdate: Can be used to customize HMR update handling.

    interfacePlugin { handleHotUpdate? (ctx: HmrContext):Array<ModuleNode> | void | Promise<Array<ModuleNode> | void>;
    }
    Copy the code

The execution order of the hooks

The hook execution of the plugin is basically the same as rollup, with vite’s unique plugin hooks interspersed between them.

The general order is as follows:

  1. config
  2. configResolved
  3. options
  4. configureServer: Note that this is at initialization time, so it can be stored beforehandconfigResolvedThe configuration of such hooks is then used here.
  5. buildStart
  6. transformIndexHtml
  7. load
  8. resolveId
  9. transform
  10. buildEnd: Triggered when packing.
  11. closeBundle: Triggered when packing.

HandleHotUpdate is the hook that is triggered each time the HMR is triggered and does not depend very much on the internal order.

Order of plug-in execution

A Vite plugin can specify an additional Enforce attribute (similar to the Webpack loader) to adjust its application order. Enforce can be pre or Post. The parsed plug-ins are sorted in the following order:

  • Alias
  • withenforce: 'pre'User plugins for
  • Vite core plug-in
  • User plug-in with no Enforce value
  • Plugin for Vite build
  • withenforce: 'post'User plugins for
  • Vite post-build plug-in (minimize, manifest, report)

Plug-in writing Practices

Now that we’ve talked about the plug-in mechanisms and specifications in Vite, let’s start writing two plug-ins. The local mock plug-in and markdown transform plug-in are examples.

Make a local Mock server plug-in

The main connection to vite from this example is to configure the development server, but it can also be used as an idea for mock data related plug-in development, so I will try to show the full code.

The local mock data in the front end can help us quickly test the front end interface, and when the real back-end interface is developed, it can be seamlessly connected. Here we mainly divided into three aspects to introduce how to write a local mock plug-in my-viet-plugin-mock.

  • Service middleware in Vite
  • Rules for writing mock files
  • Listen and load local mock files

Service middleware in Vite

Vite’s development server is built using Node’s HTTP module and introduces the connect library as a middleware module extension. Vite also provides configureServer hooks to expose server instances.

The following is a specific usage example:

import { Plugin } from 'vite'
fcuntion myPlugin(): Plugin {
  return {
      name: 'configure-server'.// Server instance
      configureServer(server) {
        // Add middleware
        server.middlewares.use((req, res) = > {
          // Custom request handling...
        })
        // Matches the prefix
        server.middlewares.use('/foo', (req, res, next) => {// Custom request handling...})}}}Copy the code

With custom middleware, development-mode requests can be intercepted and processed further.

Note: If the third parameter next is passed in, it must be called manually or it will not go to the next middleware.

Rules for writing mock files

To make it easier for mock data to communicate with the front end, we need to manually specify rules for users to follow. I limit this by using the mock path configuration of reductive forms and Typescript type definitions.

  • About pattern configuration: we need to manually specify a mock directory, our plugin will automatically traverse the entire directory and parse all the js | ts file directory, each file needs to have a default export routing configuration object, and you can also have an optional prefix export for unified path prefix.

    Like this:

    export const prefix = '/user'
    const userModule = {
    'get /getUserInfo': async() = > {// coding}}export userModule
    Copy the code
  • Route type definition: just like the userModule above, we need to create a Routes type that limits this syntax:

    import { Routes } from 'my-vite-plugin-mock'
    export const prefix = '/user'
    const userModule: Routes = {
    // Restrict key and value writing
    'get /getUserInfo': async() = > {// coding}}export userModule
    Copy the code

    Here’s the type definition:

    // ./type.ts
    import http from 'http'
    import { Connect } from 'vite'
    // Parse the array
    type Item<T> = T extends Array<infer U> ? U : never
    // All requests supported
    export type Methods = [
      'all'.'get'.'post'.'put'.'delete'.'patch'.'options'.'head'
    ]
    export type MethodProps = Item<Methods>
    // Case compatible
    export type MethodsType = MethodProps | `${Uppercase<MethodProps>}`
    
    // Returns to the front-end in JSON format
    export interfaceHandlerResult { status? :numbermessage? :stringdata? :any
    }
    
    // Since the native HTTP module does not parse the context for us, we do a layer of parsing inside the plugin to get the parameters easily
    export interface HandlerContext {
      body: Record<string.any>
      query: Record<string.string | string[] | undefined>
      params: Record<string.string>}// Handle for the route
    export type RouteHandle = (ctx: HandlerContext, req: Connect.IncomingMessage, res: http.ServerResponse) = > Promise<HandlerResult | void> | HandlerResult | void
    
    // Route Map, the key must conform to the format such as 'get/XXX '
    export type Routes = Record<`${MethodsType} The ${string}`, RouteHandle>
    // Note that the above types must be later than version 4.4 for typescript to work. If you cannot upgrade to version 4.4 for project reasons, change to the following:
    // export type Routes = Record<string, RouteHandle>
    Copy the code

    Since template strings and associative index signatures are supported in the 4.4 update to typescript, they can be typed incorrectly in later versions.

Loading a mock file

The following two steps are additional. In general, we could import and merge configuration items manually, but we added them to make it easier to write mock files in the future.

There are two aspects to loading mock files, one is to load all mock files when the service starts, and the other is to load only updated files when there are file updates in the mock directory.

So I’m going to write two functions, one for file loading and the other for directory loading (essentially both call the function to load the file). When loading the file, I can use require dynamic access (but note that we can also reference ts files directly, so there is a layer of parsing). Re-execute require when the mock file changes (note that the cache of require is removed here).

But first, we need to add two types to the previous type definition file:

// ./type.ts
// ...

// The internal handler is not exposed to the user
export type MockRoutes = Record<
  `${MethodsType} The ${string}`,
  {
    handler: RouteHandle
    method: MethodsType
  }
>
/ / reference from https://github.com/anncwb/vite-plugin-mock
// Node module processing capabilities, node itself is not defined to avoid use, but here we explicitly need to use
export interface NodeModuleWithCompile extends NodeModule {
  _compile(code: string.filename: string) :any
}
Copy the code

Here are the full parsing functions for mock files:

// ./utils.ts
import * as fs from 'fs'
import { build } from 'esbuild'
import * as path from 'path'
import { MethodsType, MockRoutes, NodeModuleWithCompile, Routes } from './type'

export interface loadMockFilesOptions {
  // Listen to the directory
  dir: string | string[]
  // Files to include or excludeinclude? :RegExp | ((filename: string) = > boolean) exclude? :RegExp | ((filename: string) = > boolean)}// Utility functions to match include and exclude
export function matchFiles({
  file,
  include,
  exclude
}: {
  file: stringinclude? :RegExp | ((filename: string) = >boolean) exclude? :RegExp | ((filename: string) = >boolean)}) :boolean {
  if (
    (exclude instanceof RegExp && exclude.test(file)) ||
    (typeof exclude === 'function' && exclude(file))
  ) {
    return false
  }
  if( include && ! ( (includeinstanceof RegExp && include.test(file)) ||
      (typeof include === 'function' && include(file))
    )
  ) {
    return false
  }
  return true
}

// Load all files, which will run the first time the plug-in loads
export async function loadMockFiles({ dir, exclude, include }: loadMockFilesOptions) :Promise<MockRoutes | null> {
  let mockRoutes: MockRoutes | null = null
  // Determine the directories to load arrays and strings separately
  if (Array.isArray(dir)) {
    mockRoutes = (
      await Promise.all(dir.map((d) = > loadDir({ dir: d, exclude, include })))
    ).reduce((prev, next) = > {
      return{... prev, ... next } }, {}as MockRoutes)
  } else {
    mockRoutes = await loadDir({ dir, exclude, include })
  }
  return mockRoutes
}

// Load the directory
async function loadDir({
  dir,
  include,
  exclude
}: Omit<loadMockFilesOptions, 'dir'> & { dir: string }) {
  const mockRoutes: MockRoutes = {}
  if (fs.existsSync(dir)) {
    const files = fs.readdirSync(dir)
    const childMockRoutesArr = await Promise.all(
      files
        .map((file) = > path.resolve(dir, file))
        .filter((file) = > matchFiles({ include, exclude, file }))
        .map((file) = > {
          const currentPath = path.resolve(dir, file)
          const stat = fs.statSync(currentPath)
          if (stat.isDirectory()) {
            // Load recursively
            return loadDir({
              include,
              exclude,
              dir: currentPath
            })
          } else {
            return loadFile(currentPath)
          }
        })
    )
    // Merge all routes
    childMockRoutesArr.forEach((childMockRoutes) = > {
      Object.keys(childMockRoutes).forEach((key) = > {
        mockRoutes[key as keyof MockRoutes] =
          childMockRoutes[key as keyof MockRoutes]
      })
    })
  }
  return mockRoutes
}

// Load the file, also called when listening for file changes
export async function loadFile(filename: string) {
  const mockRoutes: MockRoutes = {}
  // Return empty routes for a directory
  if (fs.statSync(filename).isDirectory()) {
    return mockRoutes
  }
  // resolveModule is responsible for parsing modules
  const { prefix: routePrefix, default: routes } = (await resolveModule(
    filename
  )) as{ prefix? :string
    default: Routes
  }
  typeof routes === 'object'&& routes ! = =null &&
    Object.keys(routes).forEach((routeKey) = > {
      const [method, routePath] = routeKey.split(' ')
      mockRoutes[path.join(routePrefix || ' ', routePath) as keyof MockRoutes] =
        {
          method: method as MethodsType,
          handler: routes[routeKey as keyof Routes]
        }
    })
  return mockRoutes
}

// Parse the file
async function resolveModule(filename: string) :Promise<any> {
  // If it is a TS file, use esbuild to quickly package to get js
  if (filename.endsWith('.ts')) {
    const res = await build({
      entryPoints: [filename],
      write: false.platform: 'node'.bundle: true.format: 'cjs'.target: 'es2015'
    })
    const { text } = res.outputFiles[0]
    // Change the node resolution rules
    return loadConfigFromBundledFile(filename, text)
  }
  // Be sure to remove the cache and let node reload
  delete require.cache[filename]
  return require(filename)
}

async function loadConfigFromBundledFile(
  filename: string,
  bundle: string
) :Promise<any> {
  const extension = path.extname(filename)
  const defaultLoader = require.extensions[extension]
  require.extensions[extension] = (module: NodeModule, fName: string) = > {
    if(filename === fName) { ; (module as NodeModuleWithCompile)._compile(bundle, filename)
    } else{ defaultLoader? . (module, fName)
    }
  }

  // Delete the cache
  delete require.cache[filename]
  const moduleValue = require(filename)
  // Revert to the original parsing rule
  if (defaultLoader) {
    require.extensions[extension] = defaultLoader
  }
  return moduleValue
}
Copy the code

Note: You may have noticed that we preprocessed the TS file using esbuild and then changed require. Extensions lodaer to address the introduction of TS.

The require.extensions have been deprecated since 10.6, however, because registration slows parsing of the entire application, but here since we are only using them in development mode, and require.extensions are deprecated but will probably never be removed officially, So for now, it can be used as a parsing method.

Listen for mock file changes

We use Chokidar to listen for changes to the specified folder and reload the mock file each time it changes.

// ./index.ts
import * as chokidar from 'chokidar'
import type { WatchOptions } from 'chokidar'
import { Plugin } from 'vite'
export * from './type'
import { loadMockFiles, loadFile, matchFiles } from './utils'

export interface viteMockPluginOptions extends WatchOptions {
  dir: string[] | string
  / * * *@description: Path prefix *@default: /mock
   */mockPrefix? :stringinclude? :RegExp | ((filename: string) = > boolean) exclude? :RegExp | ((filename: string) = > boolean)}function viteMockPlugin(options: viteMockPluginOptions) :Plugin {
  const { dir } = options
  return {
    name: 'my-vite-plugin-mock'.enforce: 'pre'.apply: 'serve'.async configureServer() {
      // Initialize all mock files
      let mockFiles = await loadMockFiles(options)
      // Listen to the directory
      chokidar.watch(dir, { ignoreInitial: true. options }).on('all'.(action, file) = > { // Listen for all actions, add files, modify files, etc
         // file indicates the name of the changed file
         if (
          // The matching condition is also checked here
          matchFiles({
            include: options.include,
            exclude: options.exclude,
            file
          })
        ) {
          // Cache loadingmockFiles = { ... mockFiles, ... (await loadFile(file)) }
        }
      })
     // http handler}}}export { viteMockPlugin }
export default viteMockPlugin
Copy the code

Parsing front-end requests

We learned how to load mock files and store all the routes as objects. Now we just need to parse the front-end requests and match the matching requests to the routes to complete our local mock functionality.

// ./index.ts
import * as chokidar from 'chokidar'
import type { WatchOptions } from 'chokidar'
import { parse } from 'querystring'
import { pathToRegexp, match } from 'path-to-regexp'
import { Plugin } from 'vite'
export * from './type'
import { loadFile, loadMockFiles, matchFiles } from './utils'

export interface viteMockPluginOptions extends WatchOptions {
  dir: string[] | string
  / * * *@description: Path prefix *@default: /mock
   */mockPrefix? :stringinclude? :RegExp | ((filename: string) = > boolean) exclude? :RegExp | ((filename: string) = > boolean)}function safeJsonParse<T extends Record<string | number | symbol.any> > (
  jsonStr: string,
  defaultValue: T
) {
  try {
    return JSON.parse(jsonStr)
  } catch (err) {
    return defaultValue
  }
}

// Parse the body data
function parseBody(
  req: Connect.IncomingMessage
) :Promise<Record<string.any>> {
  return new Promise((resolve) = > {
    let body = ' '
    req.on('data'.function (chunk) {
      body += chunk
    })
    req.on('end'.function () {
      resolve(safeJsonParse(body, {}))
    })
  })
}


function viteMockPlugin(options: viteMockPluginOptions) :Plugin {
  const { dir } = options
  return {
    name: 'my-vite-plugin-mock'.enforce: 'pre'.apply: 'serve'.async configureServer({ middlewares }) {
      let mockFiles = await loadMockFiles(options)
      chokidar
        .watch(dir, { ignoreInitial: true. options }) .on('all'.async (_, file) => {
          if (
            matchFiles({
              include: options.include,
              exclude: options.exclude,
              file
            })
          ) {
            // Cache loadingmockFiles = { ... mockFiles, ... (await loadFile(file)) }
          }
        })
      middlewares.use(options.mockPrefix || '/mock'.async (req, res) => {
        if (mockFiles) {
          const[url, search] = req.url! .split('? ')
          // Traverses all routing interfaces
          for (const [pathname, { handler, method }] of Object.entries(
            mockFiles
          )) {
           // Match the route
            if( pathToRegexp(pathname).test(url) && req.method? .toLowerCase() === method.toLowerCase() ) {// Parse query, params, body
              // eslint-disable-next-line no-await-in-loop
              const body = await parseBody(req)
              const query = parse(search)
              const matched = match(pathname)(url)
              // eslint-disable-next-line no-await-in-loop
              const result = await handler(
                {
                  body,
                  query,
                  params:
                    (matched && (matched.params as Record<string.string>)) ||
                    {}
                },
                req,
                res
              )
              // If the user does not use res.end() in the mock function, return the data directly
              if(! res.headersSent && result) { res.setHeader('Content-Type'.'application/json')
                res.statusMessage = result.message || 'ok'
                res.statusCode = result.status || 200
                res.end(
                  JSON.stringify({
                    message: 'ok'.status: 200. result }) ) }return}}// Return 404 if neither match
          res.setHeader('Content-Type'.'application/json')
          res.statusMessage = '404 Not Found'
          res.statusCode = 404
          res.end(
            JSON.stringify({
              message: '404 Not Found'.status: 404}))}})}}}export { viteMockPlugin }
export default viteMockPlugin
Copy the code

At this point, our mock plug-in is finally complete. There are a lot of codes, so I won’t put all of them out here. All the codes have been put on Github, and students who need them can help themselves.

Make a Markdown conversion plug-in

This plugin does the following:

  • Server (here the server is in development modeviteServer and packaging timerollupParsers) parsemdFile that intercepts to generate displayablehtmlString and directory.
  • The React stack is compatible with the Vue component import and lazy loading.
  • Ability to customize styles, can use UI component uniform styles.

Convert markDown to React components

To convert markdown to the React component, we first need to convert it to an HTML string. I used marked, a mature third-party library.

Once you’ve got the HTML string, how do you render it as a React component? React render component template (art-template) React render component template (art-template)

Here is an.art template file:

Import React, {useEffect} from 'React' // custom import {{if imports}} {{imports}} {{if}} // default display JSX, Support server to pass in component const Content = <>{{content}}</> // Original HTML const nativeContent = {{nativeContent}} // whether to use the original HTML const Native = {{isNative}} // directory const toc = {{toc}} const MarkdownComponent = ({onLoad, ClassName}) => {useEffect(() => {// provide an external function to get the content onLoad? .({ toc, html: nativeContent }) }, []) return ( <div className={`${className ? className + ' ' : ''}vite-markdown`} dangerouslySetInnerHTML={native ? { __html: nativeContent } : undefined} > {native ? Null: content} </div>)} export {toc, nativeContent as HTML} export default MarkdownComponent expose directory and original HTML contentCopy the code

Render template:

// ./utils.ts
import * as marked from 'marked'
import * as fs from 'fs'
import * as path from 'path'
import { render } from 'art-template'

/ / directory
export interface TocProps {
  level: number
  text: string
  slug: string
}

// Render props
export interface RenderProps {
  content: string
  isNative: boolean
  nativeContent: string
  toc: stringimports? :string
}


export interface MarkedRenderOptions {
  // Imports at the top of the templateimports? :string
  // marked configuration itemmarkedOptions? : marked.MarkedOptions// Whether it is native HTMLnative? :boolean
}

const MarkdownComponent = fs
  .readFileSync(path.resolve(__dirname, '.. /templates/markdown-component.art'))
  .toString()

// Write a conversion function
export function markdown2jsx(markdown: string, options: MarkedRenderOptions) {
  letrenderer = options.markedOptions? .rendererif(! renderer) { renderer =new marked.Renderer()
  }
  // Since we rely on the header to generate the directory during rendering, we need to get the original header
  const headingRender = renderer.heading
  / / directory
  const toc: TocProps[] = []
  renderer.heading = function (text, level, raw, slugger) {
    return headingRender.call(this, text, level, raw, { ... slugger,slug(. args) {
        // Unique identifier to use when getting directory anchor points
        constres = slugger.slug(... args) toc.push({ level, text,slug: res
        })
        return res
      }
    })
  }
  // Converted HTML
  const content = marked(markdown, {
    xhtml: true. options.markedOptions, renderer })const renderOptions: RenderProps = {
    isNative: options.native || false.nativeContent: JSON.stringify(content) || ' '.content: options.native ? ' ' : content,
    imports: options.imports,
    toc: JSON.stringify(toc)
  }

  return {
    content: render(MarkdownComponent, renderOptions, {
      // Do not encode, native output
      escape: false}}})Copy the code

The React component is available on the server, but the browser can’t parse JSX files. The React component is packaged into js strings by hand, which means that it needs to be packaged at runtime. In order to improve the packaging speed, we use the esbuild of Vite to package it. Because in production there is still a layer of rollup packaging).

import { transform } from 'esbuild'
import { markdown2jsx } from './utils'
// md => tsx
const { content } = markdown2jsx(code, options)
// tsx => js
transform(content, {
    loader: 'tsx'.target: 'esnext'.treeShaking: true
}).then({ code } => console.log(code))
Copy the code

Intercepting MD Files

As mentioned earlier, to parse a loaded file, we usually use the TranForm hook, which provides the content and filename of the source file. We can easily do custom parsing based on both:

// ./index.ts
import { Plugin } from 'vite'
import { SourceDescription } from 'rollup'
import { transform } from 'esbuild'
import { markdown2jsx, MarkedRenderOptions } from './utils'
/ / match markdown
const mdRegex = /\.md$/

export type mdPluginOptions = MarkedRenderOptions

function mdPlugin(options: mdPluginOptions = {}) :Plugin {
  return {
    name: 'vite-jsx-md-plugin'.enforce: 'pre'.// The react HMR plugin provides an additional layer of transform to the result, providing the HMR function in development mode
    configResolved({ plugins }) {
      const reactRefresh = plugins.find(
        (plugin) = > plugin.name === 'react-refresh'
      )
      this.transform = async function (code, id, ssr) {
        if (mdRegex.test(id)) {
          // md => tsx
          const { content } = markdown2jsx(code, options)
          // tsx => js
          const { code: transformedCode } = await transform(content, {
            loader: 'tsx'.target: 'esnext'.treeShaking: true
          })

          // Add another layer of HMR code
          // This value is null for development mode only
          const sourceDescription = (awaitreactRefresh? .transform? .call(this,
            transformedCode,
            `${id}.tsx`,
            ssr
          )) as SourceDescription
          return (
            sourceDescription || {
              code: transformedCode,
              map: { mappings: ' '}})}}as Plugin['transform']}}}export default mdPlugin
Copy the code

After some simple processing, we have completed a plug-in with loader functionality, which can help us parse the MD files that the application does not recognize into React components for use in the application. Also, since we generated the React component from a template, we can easily make adjustments to the component styles and so on:

import { defineConfig } from 'vite'
import reactRefresh from '@vitejs/plugin-react-refresh'
import viteMdPlugin from '@col0ring/vite-plugin-md'
import marked from 'marked'

const renderer = new marked.Renderer()

// We can replace the A tag directly here with the Link component of the React-Router as shown below
renderer.link = (href, title, text) = > {
  return `<Link to="${href}" title="${title}">${text}</Link>`
}

export default defineConfig({
  plugins: [
    reactRefresh(),
    viteMdPlugin({
      markedOptions: {
        renderer
      },
      // Use the a tag above, which is introduced here
      imports: ` import { Link } from 'react-router-dom' `})]})Copy the code

The plugin code is also posted on Github, you can check it out if you need it.

conclusion

This article starts with the vite plug-in mechanism and introduces the configuration and execution hooks of the Vite plug-in. In the follow-up, I spent a lot of space on plug-in practice, and developed two plug-ins for different purposes from zero to one (one is mainly used to improve the development mode, and one provides loader parsing capability). Of course, since the article is actually the logic of the plug-in itself, and only a small part of the use of vite hooks, so it can also be regarded as a tutorial on the writing of these two special plug-ins.

For more in-depth use of the Vite plugin API, check out the other great plugin repositories, and check out the community plugin set awesome- Vite.

One more thing to mention here, because the current vite ecosystem is not and very perfect, native Vite and WebPack still have a certain gap in the solution of some customization requirements, in many cases for a single project exclusive plug-in development even included in the project development part. This is why I include plug-in development as a Vite development practice.

reference

  • Rollup plug-in documentation
  • Vite official document
  • vite-plugin-mock
  • vite-plugin-mdx