The purpose of this article is to analyze the dependent pre-build features in Vite, and to attach the code addresses and official website addresses of the related features.

Depend on prebuildThe main process

  1. Prebuild dependencies before starting the server.

  2. Read user package-lock.json, yarn.lock, pnpm-lock.yaml, generate depHash.

  3. Read the prebuilt file information from the last file cache. If there is any, compare the hash obtained with depHash in the previous step. Return if the hash is the same; otherwise, rebuild. Rebuild if there is no cache or the force parameter is set.

  4. Using esbuild, the project files are scanned to obtain project dependencies.

  5. Using ESBuild, transform the modular approach that the project relies on into an ES Module approach.

  6. The converted modules are stored in cacheDir (node_module/.vite by default).

  7. When the front end requests a resource, it determines whether the requested resource is a dependency (i.e. bare import), if so, it replaces the cache file path and loads the corresponding file.

  8. After the service is started, the dependency build is redone each time a new dependency is introduced. Perform procedure 2,3,4,5.

Source code analysis

Build the start code entry

If it is not middleware mode, it is executed first before the server is startedPlugin. BuildStart hook functionTo execute the build function. Otherwise, the container is a collection of plugins that execute hook functions in sequence.

  if(! middlewareMode && httpServer) {const listen = httpServer.listen.bind(httpServer)
    // Override listen to ensure that it is executed before server starts.
    httpServer.listen = (async(port, ... args) => {try {
          // Container plugin assembly
        await container.buildStart({})//	plugin.buildStart
        await runOptimize() / / pre-built
      } catch (e) {}
      returnlisten(port, ... args) }) }else {
    await container.buildStart({})
    await runOptimize()
  }
Copy the code

RunOptimize function

_isRunningOptimizer adds build status. The optimizeDeps function returns the pre-build information returned in steps 3, 4, and 5 of the build process. _registerMissingImport returns a pre-build function that can be pre-built at any time. Rebuild when new dependencies are introduced in the running service and the _isRunningOptimizer state effectively avoids data requests at build time.

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

OptimizeDeps function

OptimizeDeps is the main function that does the following

  1. Obtain the previous pre-build information, compare this information, and decide whether to rebuild.

  2. Scan the source code, or obtain dependencies based on parameters.

  3. Leverage es-Module-Lexer flat nested source code dependencies.

  4. Resolve the user dependency optimization configuration by calling the esbuild file and storing it in cacheDir.

  5. Store the build information and return it

    function optimizeDeps( config, force=config.server.force,asCommand=false,newDeps?) {
      / /...
        
      const dataPath = path.join(cacheDir, '_metadata.json')
      // Generate the build hash
      const mainHash = getDepHash(root, config)
      const data: DepOptimizationMetadata = {
        hash: mainHash,
        browserHash: mainHash,
        optimized: {}}// The user's force parameter determines whether to rebuild each time
      if(! force) {let prevData
        try {
    	  // Load the last build information
          prevData = JSON.parse(fs.readFileSync(dataPath, 'utf-8'))}catch (e) {}
         // Compare the hash before and after, if the same, return directly
        if (prevData && prevData.hash === data.hash) {
          return prevData
        }
      }
        
          / /...
    
       The newDeps parameter is the dependency information passed in when a dependency is added after the service is started.
      let deps
      if(! newDeps) {// Use esbuild to scan source code for dependencies; ({ deps, missing } =await scanImports(config))
      } else {
        deps = newDeps
        missing = {}
      }
    
      / /...
    
      constinclude = config.optimizeDeps? .includeif (include) {
         / /... Add user-specified include
      }
        
       // Flatten dependency
      await init
      for (const id in deps) {
          flatIdDeps[id]=/ /...
        / /...
      }
        
      / /...
    
      / /... Add user-specified esbuildOptions
      const{ plugins = [], ... esbuildOptions } = config.optimizeDeps? .esbuildOptions ?? {}// Call esbuild.build to package the file
      const result = await build({
         //	...
        entryPoints: Object.keys(flatIdDeps),/ / the entry
        format: 'esm'.// Package into ESM mode
        external: config.optimizeDeps? .exclude,// Exclude files
        outdir: cacheDir,// Output address
         // ...
      })
      
      // Rewrite _metadata.json
      for (const id in deps) {
        const entry = deps[id]
        data.optimized[id] = {
          file: normalizePath(path.resolve(cacheDir, flattenId(id) + '.js')),
          src: entry,
        }
      }
    
      writeFile(dataPath, JSON.stringify(data, null.2))
    
      return data
    }
    Copy the code

The front-end gets dependencies instead of cached dependencies

Procedure: When accessing a file with imported dependencies, match the dependency name and return the dependency path under cacheDir.

  1. Introduce the preAliasPlugin plug-in in plugins when parsing config
  2. Matching the dependent name returns the name of the path to add the cache

Plugin.resolveid replaces the dependency if a value is returned, otherwise the name is passed to the next plug-in. When the dependency name is matched, change the dependency name by returning tryOptimizedResolve. /node_modules/.vite/react.js? v=7db446d6

const bareImportRE = /^[\w@](? ! . * : \ \ / /   // Match dependencies
function preAliasPlugin() {
  let server: ViteDevServer
  return {
    name: 'vite:pre-alias'.configureServer(_server) {
      server = _server
    },
    resolveId(id, _, __, ssr) {
        // If it is a dependency, add the cache path
      if(! ssr && bareImportRE.test(id)) {return tryOptimizedResolve(id, server)
      }
    }
  }
}

function tryOptimizedResolve(
  id: string,
  server: ViteDevServer
) :string | undefined {
  const cacheDir = server.config.cacheDir
  const depData = server._optimizeDepsMetadata  // Rely on build information generated in the prebuild
  if (cacheDir && depData) {
    const isOptimized = depData.optimized[id] // Find if there are dependencies
    if (isOptimized) {
      return (	// Returns the new dependency path
        isOptimized.file +
        `? v=${depData.browserHash}${
          isOptimized.needsInterop ? `&es-interop` : ` `
        }`)}}}Copy the code

Detect new dependency rebuild while running the service

The code here is too messy, and the general flow is as follows: after requesting a new dependency resource, the preAliasPlugin avoids the match and the dependency name is passed toResolvePlugin plug-inIn the judgmentWhether the file importing a dependency is also a dependencyIf yes, rebuild it.