To the question “Why do dependency prebuild?” This problem is clearly explained in the Vite documentation, so what is the general process of pre-build?

Start prebuild

From the document we know in advance before starting the service will be built, the corresponding source location in SRC/node/server/index, ts, pre-built function name is optimizeDeps

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

Start prebuild

Function optimizeDeps defined in SRC/node/optimizer index. The ts, the main process can be divided into the following steps:

  1. Determine whether prebuild is required, and if the prebuilt content is still usable, then directlyreturnIf not, continue to execute. It should be noted that the criteria for determining whether pre-built content is available arepackage.lock.jsonAnd part of vite configuration content, specific implementation ingetDepHashIn a function.

  if (!force) {
    let prevData;
    try {
      prevData = JSON.parse(fs.readFileSync(dataPath, "utf-8"));
    } catch (e) {}
    // hash is consistent, no need to re-bundle
    if (prevData && prevData.hash === data.hash) {
      log("Hash is consistent. Skipping. Use --force to override.");
      return prevData;
    }
  }
Copy the code
  1. Use esbuild to parse the entire project and get what you need to doPre-built dependenciesandResolve the dependencies in question, and assign values todepsandmissing, its main execution process in the functionscanImports(The code implementation process of this part is sorted out at the end of this part).
let deps: Record<string, string>, missing: Record<string, string>; if (! newDeps) { ({ deps, missing } = await scanImports(config)); } else { deps = newDeps; missing = {}; }Copy the code
  1. A series of processes prior to formal prebuild
    1. ifmissingIf it has a value, it gets an error, which is what we see on the consoleThe following dependencies are imported but could not be resolved.... Are they installed
    2. The configuration itemsconfig.optimizeDeps? .includeThe dependency is added todepsIf the processing fails, an error will be reported on the console
    3. ifdepsIf the value is empty, no prebuild is required. You can update the hash value of the prebuilt content directlyreturn
    4. If this command is executed, pre-build is required. The console displays the pre-build dependencies, as shown in the following figure

4. Further processingdepsgetflatIdDeps, mainly because the default ESbuild package for dependency analysis, mapping processing may be more troublesome, here mainly do two aspects of work

  1. Flatten the directory structure. For example, introducelib-flexible/flexible, while the pre-built dependency islib-flexible_flexible.js

  1. In the plugin, treat the entry file as a virtual file (this step is required by the esBuild plugin)
// esbuild generates nested directory output with lowest common ancestor base
 // this is unpredictable and makes it difficult to analyze entry / output
 // mapping. So what we do here is:
 // 1. flatten all ids to eliminate slash
 // 2. in the plugin, read the entry ourselves as virtual files to retain the
 //    path.
 const flatIdDeps: Record<string, string> = {};
 const idToExports: Record<string, ExportsData> = {};
 const flatIdToExports: Record<string, ExportsData> = {};

 await init;
 for (const id in deps) {
   const flatId = flattenId(id);
   flatIdDeps[flatId] = deps[id];
   const entryContent = fs.readFileSync(deps[id], "utf-8");
   const exportsData = parse(entryContent) as ExportsData;
   for (const { ss, se } of exportsData[0]) {
     const exp = entryContent.slice(ss, se);
     if (/export\s+\*\s+from/.test(exp)) {
       exportsData.hasReExports = true;
     }
   }
   idToExports[id] = exportsData;
   flatIdToExports[flatId] = exportsData;
 }
Copy the code
  1. Using esbuilddepsEach dependency is built and output by default tonode_modules/.viteIn the
const result = await build({ entryPoints: Object.keys(flatIdDeps), bundle: true, format: "esm", external: config.optimizeDeps? .exclude, logLevel: "error", splitting: true, sourcemap: true, outdir: cacheDir, treeShaking: "ignore-annotations", metafile: true, define, plugins: [ ...plugins, esbuildDepPlugin(flatIdDeps, flatIdToExports, config), ], ... esbuildOptions, });Copy the code
  1. Update the pre-built information and write it to the filenode_modules/.vite/_metadata.json, complete the pre-build!
for (const id in deps) {
    const entry = deps[id];
    data.optimized[id] = {
      file: normalizePath(path.resolve(cacheDir, flattenId(id) + ".js")),
      src: entry,
      needsInterop: needsInterop(
        id,
        idToExports[id],
        meta.outputs,
        cacheDirOutputPath
      ),
    };
  }

  writeFile(dataPath, JSON.stringify(data, null, 2));
Copy the code

scanImports

“Which dependencies need to be pre-built? “Is the function scanImports processing, in SRC/node/optimizer/scan. The ts, its process is simple, probably can be divided into two steps:

  1. Find the entry file (generallyindex.html)
  2. Use esbuild to do a package and find it while you’re packingdepsandmissing, and finally returnsdepsandmissing
// step 1 let entries: string[] = [] ... entries = await globEntries('**/*.html', config) ... // step 2 const plugin = esbuildScanPlugin(config, container, deps, missing, entries) const { plugins = [], ... esbuildOptions } = config.optimizeDeps? .esbuildOptions ?? {} await Promise.all( entries.map((entry) => build({ write: false, entryPoints: [entry], bundle: true, format: 'esm', logLevel: 'error', plugins: [...plugins, plugin], ... esbuildOptions }) ) ) return { deps, missing }Copy the code

As can be seen from the above, deps and missing are obtained in esbuild plugin esbuildScanPlugin, so how does this plugin do?

esbuildScanPlugin

Or in the SRC/node/optimizer/scan. The ts, the plug-in is mainly do the following two things:

  1. Handle imported modules (dependencies) inbuild.onResolveIn, specific:
  1. Set up theexternalProperty (external indicates whether the module needs to be packaged)
  2. Decide if you should joindepsormissing, the code is as follows:
. export const OPTIMIZABLE_ENTRY_RE = /\.(? :m? js|ts)$/ ... const resolved = await resolve(id, importer) if (resolved) { if (shouldExternalizeDep(resolved, id)) { return externalUnlessEntry({ path: id }) } if (resolved.includes('node_modules') || include? .includes(id)) { // dependency or forced included, externalize and stop crawling if (OPTIMIZABLE_ENTRY_RE.test(resolved)) { depImports[id] = resolved } return externalUnlessEntry({ path: id }) } else { // linked package, keep crawling return { path: path.resolve(resolved) } } } else { missing[id] = normalizePath(importer) }Copy the code

It can be seen from the above that whether modules (dependencies) are placed in DEPS, missing, and which one is placed is determined by the function resolve. The execution logic of resolve can be seen from the code as follows:

  1. Execute the hook of rollupresolveId().
  2. Execute pluginContainer for ViteresolveId()
  3. And finally hereresolve()

Since I am not very clear about the processing logic of this paragraph, I can only simply understand it as:

  1. resolveIf it fails, it will be putmissing
  2. resolvecontainsnode_modulesI understand it to mean put onnode_modulesDirectory) or configuration items in ViteincludeAnd in theOPTIMIZABLE_ENTRY_REIt’s going to go straight indepsWaiting for the package, no further down.

Deps and Missing required by pre-construction have been collected here.

  1. Process file contents inbuild.onLoadIn, specific:
  1. for.html .vue svelteThis kind of file with JS logic, need to pull out the JS part of it, useThe import and exportSyntax wrap and return
  2. For different files (JS, TS, JSX…) , load different loader resolution

Prebuild results

The pre-built results are placed in node_modules/.vite/, as shown in the following figure, containing two aspects of information:

  1. _metadata.jsonIs the information of some “versions” and dependent packages generated by this pre-build, as shown in the figure below:

  1. xx.js, xxx.js.mapPackaging results for individual dependent packages

END

Pre-built part of the code implementation is probably like this, the article synchronized in the Vite source code reading, on the vite source code related learning will be recorded here, welcome to discuss exchanges, thank you 🙏