Vite source analysis – Start Vite

The Vite framework is divided into two parts. One is an efficient development experience that relies on esBuild during the development phase. Vite is called Serve Command. The other part is that the build phase relies on rollup to compile the final product, which Vite calls Build Command. Build Command will not be introduced, but only the core serve Command that is recognized by everyone.

This article analyzes the vite source version is V2.4.1 and the latest version is almost the same, but the v1 version is very different, so the analysis of this article is relatively new.

Overall, this series will be divided into three parts. This section will introduce the basic modules used by Vite, including config configuration processing, plugin processing and execution, module-dependent moduleGraph building, pre-build analysis, etc. Among them, pre-build is the core content. The second part of requesting resources describes how resources can be requested through the browser and how the Vite service handles the request once it is received. The third hot update, this part mainly introduces how to carry out hot update in Vite serve mode, in fact, similar to Webpack. Of course starting Vite is the most important.

The front background

Here is a brief description of the operating environment and the simple project on which the following analysis is based:

package.json

{
  "version": "0.0.1"."scripts": {
    "dev": "vite"."build": "vite build"
  },
  "dependencies": {
    "lodash": "^ 4.17.21"."lodash-es": "^ 4.17.21"."vue": "^ 3.0.5"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": ^ "1.2.3"."@vitejs/plugin-vue-jsx": "^ 1.1.5." "."@vue/compiler-sfc": "^ 3.0.5"."vite": "^ 2.3.7." "}}Copy the code

vite.config.js

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue(), vueJsx()],
});
Copy the code

src/main.js

import { createApp } from "vue";
import App from "./App";
import { toString, toArray } from "lodash-es";

console.log(toString(123));
console.log(toArray([]));

createApp(App).mount("#app");


Copy the code

src/App.jsx

import { defineComponent } from "vue";

export default defineComponent({
  setup() {
    return () = > {
      return <div>hello vite</div>; }; }});Copy the code

index.html

<! DOCTYPE html><html lang="en">
<head>
  <meta charset="UTF-8" />
</head>

<body>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body>
</html>
Copy the code

Please refer to the official document for details about how to install it, which is probably like this file structure:

Run vite

After you run yarn run dev in the root directory, that is, vite, the node_modules/. Bin /vite script is executed. The script is implementation node_modules/vite/dist/node/cli. Js in the cli file command (‘/root) and this is the default execution of the script. The following code is executed:

Source location: vite v2.4.1 / packages/vite/SRC/node/cli. Ts 80

 const server = await createServer({
  root,
  base: options.base,
  mode: options.mode,
  configFile: options.config,
  logLevel: options.logLevel,
  clearScreen: options.clearScreen,
  server: cleanOptions(options) as ServerOptions
});
await server.listen();
Copy the code

Since we usually execute vite directly without any parameters, it is equivalent to:

 const server = await createServer({
  root: undefined.base: undefined.mode: undefined.configFile: undefined.logLevel: undefined.clearScreen: undefined.server: undefined});await server.listen();
Copy the code

Next we’ll take a closer look at the createServer process and how the Listen function executes. CreateServer involves initializing the Vite Config configuration file, building the plugin runtime container, initializing module dependencies, creating a server, and adding, more importantly, transformMiddleware. The Listen process mainly executes the Plugin’s buildStart hook and the pre-built optimizeDeps process. These parts will be explained in the following sections.

CreateServer – Creates a service

Below is the source code for createServer, which will be described in the following sections for the more important parts.

export async function createServer(
  inlineConfig: InlineConfig = {}
) :Promise<ViteDevServer> {
  /** * 1. Initialize config * Initialize the config file, which contains the initialization process of plug-ins, including filtering plug-ins and sorting plug-ins. The plug-in's Config hook deep merge of config is also performed. * /
  const config = await resolveConfig(inlineConfig, 'serve'.'development')
  const root = config.root
  const serverConfig = config.server
  const httpsOptions = await resolveHttpsConfig(config)
  let { middlewareMode } = serverConfig
  if (middlewareMode === true) {
    middlewareMode = 'ssr'
  }
  /** * 2. Create connect service */
  const middlewares = connect() as Connect.Server
  const httpServer = middlewareMode
    ? null
    : await resolveHttpServer(serverConfig, middlewares, httpsOptions)
  
    // HMR will not be introduced at this time
  const ws = createWebSocketServer(httpServer, config, httpsOptions)
  const{ ignored = [], ... watchOptions } = serverConfig.watch || {}const watcher = chokidar.watch(path.resolve(root), {
    ignored: ['**/node_modules/**'.'**/.git/**'. ignored],ignoreInitial: true.ignorePermissionErrors: true.disableGlobbing: true. watchOptions })as FSWatcher

  const plugins = config.plugins
  BuildStart, resolveId, load, transform */
  const container = await createPluginContainer(config, watcher)
  /** * create module dependencies to describe the dependencies between modules, where each module contains id, URL, file identifier, The importer module introduces which modules and importedModules are referenced by those modules. TransformResult contains code, map, etag */
  const moduleGraph = new ModuleGraph(container) // a tool for managing dependencies between modules
  const closeHttpServer = createServerCloseFn(httpServer)

  // eslint-disable-next-line prefer-const
  let exitProcess: () = > void

  /** * Create server */
  const server: ViteDevServer = {
    config: config,
    middlewares,
    get app() {
      config.logger.warn(
        `ViteDevServer.app is deprecated. Use ViteDevServer.middlewares instead.`
      )
      return middlewares
    },
    httpServer,
    watcher,
    pluginContainer: container,
    ws,
    moduleGraph,
    transformWithEsbuild,
    transformRequest(url, options) {
      return transformRequest(url, server, options)
    },
    transformIndexHtml: null as any,
    ssrLoadModule(url) {
      if(! server._ssrExternals) { server._ssrExternals = resolveSSRExternal( config, server._optimizeDepsMetadata ?Object.keys(server._optimizeDepsMetadata.optimized)
            : []
        )
      }
      return ssrLoadModule(url, server)
    },
    ssrFixStacktrace(e) {
      if (e.stack) {
        e.stack = ssrRewriteStacktrace(e.stack, moduleGraph)
      }
    },
    listen(port? : number, isRestart? : boolean) {
      return startServer(server, port, isRestart)
    },
    async close() {
      process.off('SIGTERM', exitProcess)

      if(! process.stdin.isTTY) { process.stdin.off('end', exitProcess)
      }

      await Promise.all([
        watcher.close(),
        ws.close(),
        container.close(),
        closeHttpServer()
      ])
    },
    _optimizeDepsMetadata: null._ssrExternals: null._globImporters: {},
    _isRunningOptimizer: false._registerMissingImport: null._pendingReload: null
  }

  server.transformIndexHtml = createDevHtmlTransformFn(server)

  exitProcess = async() = > {try {
      await server.close()
    } finally {
      process.exit(0)
    }
  }

  process.once('SIGTERM', exitProcess)

  if(process.env.CI ! = ='true') {
    process.stdin.on('end', exitProcess)
    process.stdin.resume()
  }

  watcher.on('change'.async (file) => {
    file = normalizePath(file)
    // invalidate module graph cache on file change
    moduleGraph.onFileChange(file)
    if(serverConfig.hmr ! = =false) {
      try {
        await handleHMRUpdate(file, server)
      } catch (err) {
        ws.send({
          type: 'error'.err: prepareError(err)
        })
      }
    }
  })

  watcher.on('add'.(file) = > {
    handleFileAddUnlink(normalizePath(file), server)
  })

  watcher.on('unlink'.(file) = > {
    handleFileAddUnlink(normalizePath(file), server, true)})// apply server configuration hooks from plugins
  const postHooks: ((() = > void) | void=) [] []for (const plugin of plugins) {
    if (plugin.configureServer) {
      postHooks.push(await plugin.configureServer(server))
    }
  }

  // Internal middlewares ------------------------------------------------------
  / /...

  /** * 5. Add transformMiddleware, which is the */ associated with browser request modules
  middlewares.use(transformMiddleware(server))

  // run post config hooks
  // This is applied before the html middleware so that user middleware can
  // serve custom content instead of index.html.
  postHooks.forEach((fn) = > fn && fn())


  const runOptimize = async() = > {if (config.cacheDir) {
      server._isRunningOptimizer = true
      try {
        server._optimizeDepsMetadata = await optimizeDeps(config)
      } finally {
        server._isRunningOptimizer = false
      }
      server._registerMissingImport = createMissingImporterRegisterFn(server)
    }
  }

  if(! middlewareMode && httpServer) {// overwrite listen to run optimizer before server start
    const listen = httpServer.listen.bind(httpServer)
    /** * 6. Listen for the service callback, it is important to execute the optimizeDeps pre-build process in addition to executing the plugin's buildStart hook */
    httpServer.listen = (async(port: number, ... args: any[]) => {try {
        await container.buildStart({})
        await runOptimize()
      } catch (e) {
        httpServer.emit('error', e)
        return
      }
      returnlisten(port, ... args) })as any

    httpServer.once('listening'.() = > {
      // update actual port since this may be different from initial value
      serverConfig.port = (httpServer.address() as AddressInfo).port
    })
  } else {
    await container.buildStart({})
    await runOptimize()
  }

  return server
}
Copy the code

ResolveConfig – Initializes config

Source location: vite/v2.4.1 / packages/vite/SRC/node/config. The ts

The first line of createServer’s execution is to call the resolveConfig method to get the config initialized.

const config = await resolveConfig(inlineConfig, 'serve'.'development');
Copy the code

The specific resolveConfig code is relatively simple and does not have much relationship with the main process, the most important is about plugins processing.

Initialize the plug-in

Extract the code about plugin:

// 1. Filter out the plugins required by the development phase (command as serve)
const rawUserPlugins = (config.plugins || []).flat().filter((p) = > {
  returnp && (! p.apply || p.apply === command)// Command is serve or build
});
// 2. Obtain pre, Normal, and POST plugins
const [prePlugins, normalPlugins, postPlugins] = sortUserPlugins(rawUserPlugins);
// 3. Queue plugin
const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins];
// Execute each of the config functions in the user-defined plugin from front to back, then merge into the existing config to obtain the new config
for (const p of userPlugins) {
  if (p.config) {
    const res = await p.config(config, configEnv) // Execute the config hook to update the configuration information to merge the new config with the existing config
    if (res) {
      config = mergeConfig(config, res)
    }
  }
}
// 4. Sort with vite built-in plugins to get the final plugin that needs to be executed
resolved.plugins = [
  // alias handles related plug-ins
  preAliasPlugin(),
  aliasPlugin({ entries: config.resolve.alias }),
  // User-defined pre type plug-in. prePlugins,// The core plug-in is mainly related to processing request filesdynamicImportPolyfillPlugin(config), resolvePlugin({ ... config.resolve,root: config.root,
    isProduction: config.isProduction,
    isBuild,
    ssrTarget: config.ssr? .target,asSrc: true}), htmlInlineScriptProxyPlugin(), cssPlugin(config), config.esbuild ! = =false ? esbuildPlugin(config.esbuild) : null,
  jsonPlugin(
    {
      namedExports: true. config.json }, isBuild ), wasmPlugin(config), webWorkerPlugin(config), assetPlugin(config),// User-defined plug-in. normalPlugins,// Build plugins
  definePlugin(config),
  cssPostPlugin(config),
  // A plug-in that needs to be run after the build. postPlugins,// Post plugin to generate minimize, manifest, report
  clientInjectionsPlugin(config),
  importAnalysisPlugin(config)
].filter(Boolean);
Copy the code

The general steps of plugin processing are as follows:

  1. RawUserPlugins: Config. plugins refer to the plugin passed in to the user’s vite configuration file. Our plugins are [vue(), vueJsx()]. Please refer to the vite plug-in for the attributes of plugins. If the apply attribute is not set, it will be executed in both serve and build mode by default. The result is rawUserPlugins. Since vue and vueJsx apply are empty, rawUserPlugins are still [vue(), vueJsx()].

  2. SortUserPlugins: obtain the plugins that need to be executed at this moment, and then divide the plugins into pre, Normal, and POST types according to the Enforce plug-in field. In that order, the final list of user plug-ins, userPlugins, is obtained.

  3. Execute the plugin config hook: Execute the plugin’s Config hook functions in the order listed in the userPlugins plugin list, and the returned objects are deeply merged into the existing Config. For example, vue hooks require global variables such as __VUE_OPTIONS_API__ during the build process. The config hook can be written like this:

    config(config) {
      return {
        define: {
          __VUE_OPTIONS_API__: true.__VUE_PROD_DEVTOOLS__: false}}; }Copy the code

    This way the define field is merged into the Config object.

  4. To get the final plugins: Some built-in vite plug-ins and user-defined plug-ins according to Alias, Pre type user plug-in, Vite core plug-in, normal type user plug-in, Vite build plug-in, post type user plug-in, vite post-build plug-in (minimize, manifest, Report) in order to form the final list of plugins.

In the end the config

Finally, we get the config as:

{
  base:'/'.// The root directory of the project, usually the project directory
  root:'... '.// Build the cache folder, which by default is the. Vite file under node_modules in the project directory
  cacheDir:'... /node_modules/.vite'.// The command mode is serve or build
  command:'serve'.// Environment development, production, same as process.env.node_env
  mode:'development'.// Vite configuration file location
  configFile:'... /vite.config.js'.// The server needs to run the public folder
  publicDir:'... /public'.configFileDependencies: ['vite.config.js'].// This is a user-defined configuration attribute set using the vUE plug-in, which is used in the vue plug-in
  define: {__VUE_OPTIONS_API__: true.__VUE_PROD_DEVTOOLS__: false
  },
  env: {BASE_URL: '/'.MODE: 'development'.DEV: true.PROD: false
  },
  esbuild: {include: /\\.ts$/},
  // createServer passes in command line arguments, both of which are undefined
  inlineConfig: {root: undefined.base: undefined.mode: undefined.configFile: undefined.logLevel: undefined,... },isProduction: false.optimizeDeps: {esbuildOptions: {... }}// A collection of plug-ins, resulting in the list of plug-ins described above
  plugins: [],... }Copy the code

CreatePluginContainer – Creates a plugin to run the container

Source location: vite/v2.4.1 / packages/vite/SRC/node/server/pluginContainer ts

The plugin runtime environment is created after createServer gets the config.

First of all, the rollup plugin system is used in Vite, but the rollup plugin system has special encapsulation, such as the rollup context plugin container concept, Vite has special encapsulation, Some hooks such as setAssetSource and getFileName that directly throw WARN cannot be used. Hooks such as moduleParsed will not be used in development due to AST performance degradation. The following hooks have also been added or overwritten: buildStart, resolveId, Load, Transform, buildEnd, closeBundle. Here is the source code for some of the more important hooks:

buildStart

  async buildStart() {
    await Promise.all(
      plugins.map((plugin) = > {
        if (plugin.buildStart) {
          return plugin.buildStart.call(
            new Context(plugin),
            container.options
          )
        }
      })
    )
  }
Copy the code

The main purpose of this hook is to do some work that needs to be done before the service is built at startup time. All hook buildStart methods are executed asynchronously and concurrently.

resolveId

async resolveId(rawId, importer = join(root, 'index.html')) {
  let id = null;
  let partial = {};
  for (const plugin of plugins) {
    if(! plugin.resolveId)continue
    const result = await plugin.resolveId.call(
      ctx, // plugin container environment, you can use some environment variables in resolveId
      rawId,
      importer,
      {}
    )
    if(! result)continue;

    if (typeof result === 'string') {
      id = result;
    } else {
      id = result.id;
      Object.assign(partial, result);
    }
    break;
  }

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

ResolveId (resolveId, resolveId, resolveId, resolveId, resolveId, resolveId, resolveId, resolveId, resolveId, resolveId, resolveId, resolveId, resolveId, resolveId, resolveId, resolveId, resolveId, resolveId, resolveId, resolveId, resolveId, resolveId

load


  for (const plugin of plugins) {
    if(! plugin.load)continue;
    ctx._activePlugin = plugin;
    const result = await plugin.load.call(ctx, id);
    if(result ! =null) {
      returnresult; }}return null;
},
Copy the code

This hook is called on each incoming module request, mainly to load resources, and executes the load method of each hook sequentially in order of the plug-in. If there is a return, it returns null.

transform

async transform(code, id, inMap) {
  for (const plugin of plugins) {
    if(! plugin.transform)continue
    let result;
    try {
      result = await plugin.transform.call(ctx, code, id);
    } catch (e) {
      ctx.error(e)
    }
    if(! result)continue;
    if (typeof result === 'object') {
      code = result.code || ' '
      if (result.map) ctx.sourcemapChain.push(result.map) / / sourcemap related
    } else {
      code = result
    }
  }
  return {
    code,
    map: ctx._getCombinedSourcemap()
  }
},
Copy the code

This hook is called on each incoming module request to process the loaded source code, accept the file source code, and then execute the transform method sequentially in plug-in order. The first plug-in processes the code as input to the last plug-in until all plug-ins are finished executing. The corresponding SourcemAP is also generated using the rollup capability during this process.

So much for vite’s more important plug-in runtime containers. These plug-in hook calls will be explained in depth in the following analysis. For now, I have a general impression of the Plugin coantainer and each stage.

ModuleGraph – Module dependency management

The management file relies on the ModuleGraph structure. For each module, such as index, A, B, and C, their module object data structure is:

ModuleNode {
  url: string
  id: string
  file: string
  type: 'js' | 'css'
  importers = new Set<ModuleNode>() // Mods are introduced by those modules
  importedModules = new Set<ModuleNode>() // The mod introduced those modules
  acceptedHmrDeps = new Set<ModuleNode>() // HMR Accepted those modules
  transformResult: TransformResult | null = null  // code map etag
}
Copy the code

Id is the unique identifier obtained by the address of the imported module through the resolveId method of plugin. Urls are simply addresses obtained relative to root, and import addresses in js files parsed in browsers are urls. File is the absolute address of an ID after all search and hash are removed.

Each module references each other to form a graph, and modules are maintained by using importedModules fields that identify which modules are referenced by them. AcceptedHmrDeps is related to hot updates, and the code explicitly writes whether a file is updated or not determined by those modules.

In addition to module and moduleGraph, there are idToModuleMap, urlToModuleMap, and fileToModuleMap

These maps establish mappings between IDS, urls, files, and Modules to make lookups efficient. This is also an application that uses graph storage structures and hash indexes to speed up queries.

In the follow-up introduction of a JS is how to deal with the introduction of the following methods specific use.

OptimizeDeps – pre-built

Source location: vite v2.4.1 / packages/vite/SRC/node/optimizer index. The ts

CreateServer completes server creation by executing server.listen, which executes the buildStart hooks for all plug-ins and then optimizeDeps pre-build. Why do you need pre-build? There are two main reasons:

1. For performance: In this process, we mainly avoid module dependence such as Lodash which contains a lot of modules, resulting in frequent multiple module requests blocking the normal execution speed of the project. Therefore, we need to pre-build the dependencies before the execution of the project. We pack the dependencies package into the cache file to improve the speed of request and project opening.

2. To be compatible with CJS modules: Browsers load modules with ESM modules by default, but if they rely on a CJS module, Vite needs to convert the CJS module to ESM modules during the pre-build process.

The overall pre-built flow chart is as follows, and we interpret it one by one according to the project.

OptimizeDeps prebuild process is simply to scan project modules that depend on node_modules such as vue lodash-es and pack those DEPs in advance. Then, the contents of DEps, package management lock and vite. Config are hash generated to uniquely identify the build. If the hash generated again is inconsistent, it indicates that the project dependencies have changed, and the previous build needs to be abandoned and pre-built again.

getDepHash

Json, yarn.lock, and pnpm-lock.yaml files from config.root, which is the root directory of the project. Combine this content with some items in viet.config into a string and get the 8-bit hash value of the string.

Compare the hash

Json file, and compare the hash value with the generated hash. If the hash value is consistent, it indicates that the dependencies and vite configuration of the project have not changed. Then, the pre-built content can be directly used and returned without the subsequent pre-build. Otherwise it will enter the pre-build process.

Before proceeding to the build process, clear the previous build cache, that is, clear the node_modules/. Vite file, and set packs. json type to module so that the browser can import and load project modules.

scanImports

This is the first procedure to execute an esbuild build. In the case of not considering multiple entry, the build code is as follows, of course vite also supports multiple entry:

esbuild.build({
    write: false.// Do not write to disk
    entryPoints: [entry],
    bundle: true.// Js needs to be packaged
    format: 'esm'.// Format all dependent files into ESM mode
    plugins: [esbuildScanPlugin]
})
Copy the code

Here entry can be multiple entries, of which single entry sources are mainly as follows:

  let entries: string[] = []

  constexplicitEntryPatterns = config.optimizeDeps? .entriesconstbuildInput = config.build.rollupOptions? .input// Get entries from optimizeDeps and search globally for the entry list
  if (explicitEntryPatterns) {
    entries = await globEntries(explicitEntryPatterns, config)
  } else if (buildInput) {
  Input and root take absolute values, so if the project is configured with root and input, be careful not to find the entry and fail to prebuild
    const resolvePath = (p: string) = > path.resolve(config.root, p)
    if (typeof buildInput === 'string') {
      entries = [resolvePath(buildInput)]
    } else if (Array.isArray(buildInput)) {
      entries = buildInput.map(resolvePath)
    } else if (isObject(buildInput)) {
      entries = Object.values(buildInput).map(resolvePath)
    } else {
      throw new Error('invalid rollupOptions.input value.')}}else {
  // By default, HTML files are searched globally. For monorepo projects or multiple index. HTML projects, it is best to specify an explicit entry, otherwise it will consume pre-build time
    entries = await globEntries('**/*.html', config)
  }

Copy the code

Here is the esbuildScanPlugin code:

const scriptModuleRE = /(
      \b[^>s;
const scriptRE = /(
      \b(\s[^>s;
const srcRE = /\bsrc\s*=\s*(? :"([^"]+)"|'([^']+)'|([^\s'">]+))/im;
const JS_TYPES_RE = / \. (? :j|t)sx? $|\.mjs$/;
const externalUnlessEntry = ({ path }) = > ({
  path,
  external: !entries.includes(path) // True for all but index.html, indicating that no further build will take place until the end of the path module
});

const resolve = async(id: string, importer? : string) => {const key = id + (importer && path.dirname(importer))
  if (seen.has(key)) {
    return seen.get(key)
  }
  const resolved = await container.resolveId(
    id,
    importer && normalizePath(importer)
  )
  constres = resolved? .id seen.set(key, res)return res
}

function esbuildScanPlugin() {
return {
  name: 'vite:dep-scan'.setup(build) {
    // onResolve-html
    // Return {path: '/[root]/index.html', namespace: 'HTML'} before loading index.html
      build.onResolve({ filter: htmlTypesRE }, async ({ path, importer }) => {
          return {
              path: await resolve(path, importer),
              namespace: 'html'
          };
      });
      // onLoad-html
      {loader: 'js', contents: 'import "/ SRC /main.js"'}
      build.onLoad({ filter: htmlTypesRE, namespace: 'html' }, async ({ path }) => {
          let raw = fs__default.readFileSync(path, 'utf-8');
          const isHtml = path.endsWith('.html');
          const regex = scriptModuleRE;
          regex.lastIndex = 0;
          let js = ' ';
          let loader = 'js';
          let match;
          while ((match = regex.exec(raw))) {
              const [, openTag, htmlContent, scriptContent] = match;
              // openTag: <script type="module" src="/src/main.js">
              const srcMatch = openTag.match(srcRE);
              if (srcMatch) {
                  const src = srcMatch[1] || srcMatch[2] || srcMatch[3]; // /src/main.js
                  js += `import The ${JSON.stringify(src)}\n`; }}return {
              loader, // js
              contents: js // import "/src/main.js"
          };
      });
      // onResolve-node_modules
      // Execute onResolve before loading the parse build if the loaded module is a word or a third party module that begins with @
      build.onResolve({
          // avoid matching windows volume
          filter: /^[\w@][^:]/
      }, async ({ path: id, importer }) => {
        if (depImports[id]) {
          return externalUnlessEntry({ path: id })
        }
        const resolved = await resolve(id, importer)
        if (resolved) {
          if (shouldExternalizeDep(resolved, id)) {
            return externalUnlessEntry({ path: id })
          }
          // Modules in the node_modules directory will be put into the depImports array, and the declared optimize.includs will also be pre-built
          if (resolved.includes('node_modules') || include? .includes(id)) {if (OPTIMIZABLE_ENTRY_RE.test(resolved)) {
              depImports[id] = resolved
            }
            return externalUnlessEntry({ path: id })
          } else {
            return {
              path: path.resolve(resolved)
            }
          }
        }
      });
      // onResolve-any
      // Execute onResolve before any file is loaded, returns an absolute path, and esBuild loads the module
      build.onResolve({
          filter: /. * /
      }, async ({ path: id, importer }) => {
          // use vite resolver to support urls and omitted extensions
          const resolved = await resolve(id, importer);
          if (resolved) {
              if (shouldExternalizeDep(resolved, id)) {
                  return externalUnlessEntry({ path: id });
              }
              const namespace = htmlTypesRE.test(resolved) ? 'html' : undefined;
              return {
                  path: path__default.resolve(cleanUrl(resolved)),
                  namespace
              };
          }
          else {
              // resolve failed... probably unsupported type
              return externalUnlessEntry({ path: id }); }});// onLoad-js
     build.onLoad({ filter: JS_TYPES_RE }, ({ path: id }) = > {
            let ext = path__default.extname(id).slice(1);
            if (ext === 'mjs')
                ext = 'js';
            let contents = fs__default.readFileSync(id, 'utf-8');
            if (ext.endsWith('x') && config.esbuild && config.esbuild.jsxInject) {
                contents = config.esbuild.jsxInject + `\n` + contents;
            }
            if (contents.includes('import.meta.glob')) {
                return transformGlob(contents, id, config.root, ext).then((contents) = > ({
                    loader: ext,
                    contents
                }));
            }
            return {
                loader: ext, contents }; }); }}};Copy the code

The process to execute for esbuild starts with the entry index.html file,

  1. The onresolve-html hook returns {path: ‘/[root]/index.html’, namespace: ‘HTML’}

  2. With the onload-html hook, parse the HTML file to find all the script tags and parse out the SRC dependency/SRC /main.js. Return {loader: ‘js’, contents: ‘import “/ SRC /main.js”‘}, and esbuild will parse the contents using the JS parsing engine.

  3. / SRC /main.js passes onresolve-any hook, id is the file path, importer is /[root]/index.html path, then calculates the absolute path of main.js according to id and importer. {path: ‘[root]/ SRC /main.js’, namespace: undefined}

  4. The onload-js hook returns {loader:’js’, contents: main file contents}. Esbuild parses contents using the parse JS engine

  5. Resolve-node_modules hook (vue) Vue will resolve the absolute path for node_modules/vue/dist/vue runtime. Esm – bundler. Js, so need to add the key to the vue depImports, value is an absolute path. Return {path, external: false}, return {path, external: ResolveId (resolveId, resolveId, resolveId, resolveId, resolveId, resolveId);

  6. Handle the ‘./App’ import and use the onResolve — any hook to get {path: ‘[root]/ SRC/app. JSX ‘,namespace}, then go to the onload-js hook to return {loader:’ JSX ‘, contents: App contents}, esBuild will parse the App file via JSX parsing module.

  7. The process is similar to VUE for loDash-ES and so on.

The final DEPS is:

{
  vue: "[root]/node_modules/vue/dist/vue.runtime.esm-bundler.js"."lodash-es": "[root]/node_modules/lodash-es/lodash.js",}Copy the code

In short, analyze the third-party modules that depend on in the project, and then get the DEPS object with the module name as the key and the absolute address as the value.

browserHash

Then browserHash is the 8-bit hash value obtained by combining the hash with deps as a string. The request for the browser file will carry the hash, and if the HAHS is inconsistent it will be rebuilt.

So the timeliness of the pre-built file cache here depends on: 1) Dependecies third-party libraries in Packages. json, 2) Lock files for package management, and 3) Vite configuration items.

esbuild.build

const result = await esbuild.build({
    entryPoints: Object.keys(deps), // ['vue', 'lodash-es']
    bundle: true.format: 'esm'.splitting: true.sourcemap: true.outdir: "[root]/node_modules/.vite".treeShaking: 'ignore-annotations'.metafile: true.plugins: [
        esbuildDepPlugin(deps, flatIdToExports, config)
    ]
});
Copy the code

Before build is executed, each module of DEPS is analyzed by esbuild Parser process to get the dependencies of the module and whether any exports are saved in flatIdToExports. The flatIdToExports file describes the deps dependencies that each exported from those files.

Then the esbuild build process is executed, the entry file is for each DEps, and the build output file is in the.vite directory. The specific build process is defined using the esbuildDepPlugin plugin.

The code for the esbuildDepPlugin is as follows:

function esbuildDepPlugin(qualified, exportsData, config) {
  return {
    name: "vite:dep-pre-bundle".setup(build) {
      // onResolve-node_modules
      build.onResolve({ filter: /^[\w@][^:]/ }, async ({ path: id, importer, kind, resolveDir }) => {
        {path: vue/lodash-es, namespaces: 'dep'}
        if (id in qualified) {
          return {
            path: flatId,
            namespace: "dep"}; }// For non-entry modules, go back to the absolute path, return {path: absolute path}, and esBuild will parse the module
        const resolved = await resolve(id, importer, kind);
        if (resolved) {
          return {
            path: path__default.resolve(resolved), }; }});// onLoad-entry
      build.onLoad({ filter: /. * /, namespace: "dep" }, ({ path: id }) = > {
        // Get the relative path of the entry module
        const entryFile = qualified[id];
        let relativePath = normalizePath$4(path__default.relative(root, entryFile));
        if(! relativePath.startsWith(".")) {
          relativePath = `. /${relativePath}`;
        }
        let contents = "";
        const data = exportsData[id];
        const [imports, exports] = data;
        // If it is a CJS module, the code returned is' export default require("${relativePath}");
        if(! imports.length && !exports.length) {
          // cjs
          contents += `export default require("${relativePath}"); `;
        } else {
          // If the esM module contains the default export, the code is import d from "${relativePath}"; export default d;
          if (exports.includes("default")) {
            contents += `import d from "${relativePath}"; export default d; `;
          }
          Export * from "${relativePath}"
          if (data.hasReExports || exports.length > 1 || exports[0]! = ="default") {
            contents += `\nexport * from "${relativePath}"`; }}let ext = path__default.extname(entryFile).slice(1);
        if (ext === "mjs") ext = "js";
        // Esbuild parses the content
        return {
          loader: ext,
          contents,
          resolveDir: root, }; }); }}; }Copy the code

Export default require(“${relativePath}”); export default require(“${relativePath}”);

  • If no import or export is considered to be a CJS, the content is returned.

  • If any import or export is considered to be ESM:

    • Import d from “${relativePath}”; export default d;

    • Export * from “${relativePath}”

Return {loder: js, contents:code, resolveDir: root}

The idea is to pack duplicate files into multiple chunks. The contents of the DEPS file are then parsed and all the imported files related to the file are packaged into one chunk. For example, vue packages output modules that include ‘@vue/ Runtime-core ‘ ‘@vue/runtime-common’ ‘@vue/shared’ ‘@vue/reactivity’ and so on.

Generate the final metadata file and write it to the cache directory.vite.

{
  hash: "b0f10227".browserHash: "3d578768".optimized: {
    vue: {
      file: "/Users/mt/Documents/storehouse/vite/demo-0-vite-vue3/node_modules/.vite/vue.js".src: "/Users/mt/Documents/storehouse/vite/demo-0-vite-vue3/node_modules/vue/dist/vue.runtime.esm-bundler.js".needsInterop: false,},"lodash/toString": {
      file: "/Users/mt/Documents/storehouse/vite/demo-0-vite-vue3/node_modules/.vite/lodash_toString.js".src: "/Users/mt/Documents/storehouse/vite/demo-0-vite-vue3/node_modules/lodash/toString.js".needsInterop: true.// Identifies a CJS file that requires dependencies to be built
    },
    "lodash/toArray": {
      file: "/Users/mt/Documents/storehouse/vite/demo-0-vite-vue3/node_modules/.vite/lodash_toArray.js".src: "/Users/mt/Documents/storehouse/vite/demo-0-vite-vue3/node_modules/lodash/toArray.js".needsInterop: true,,}}}Copy the code

This is the end of the pre-built process. To summarize the pre-build process, each pre-build has a unique hash and browerHash, determined by the package managed Lock file, the VIE.config configuration item, and even the third-party library used in the project. If one of the three changes, the hash changes and needs to be prebuilt again. This is also the policy mechanism for vite file caching. For prebuild, scanImport scans third-party packages in node_modules that the project code depends on, and then esbuild these packages as an entry point to get packaged prebuild resources. Optimized and Hash the packaged content description is written to metadata.json.