Take a look at the official website

When you first start Vite, you may notice the following message printed:

Optimizable dependencies detected: React, react-dom pre-bundling them to speed up dev server page load This will be run only when your dependencies have changedCopy the code

Preconstruction effect

CommonJS and UMD compatibility

During development, Vite’s development server treats all code as native ES modules. Therefore, Vite must first convert dependencies published as CommonJS or UMD to ESM.

Vite performs intelligent import analysis when converting CommonJS dependencies, so that even if the export is allocated dynamically (like React), the import by name will behave as expected:

// As expected
import React, { useState } from 'react'
Copy the code

performance

Vite converts ESM dependencies that have many internal modules into a single module to improve subsequent page loading performance; Reduce network requests.

The cache

File system caching

Vite caches pre-built dependencies to node_modules/.vite. It determines whether the pre-build step needs to be rerun based on several sources:

  • package.jsonIn thedependenciesThe list of
  • Lockfile for package manager, for examplepackage-lock.json.yarn.lockOr,pnpm-lock.yaml
  • May be invite.config.jsThe value has been configured in related fields

You only need to rerun the prebuild if one of the above changes.

If for some reason you want to force Vite to rebuild dependencies, you can either start the development server with the –force command-line option, or manually delete the node_modules/.vite directory.

Browser cache

Parsed dependent requests are strongly cached with HTTP header max-age=31536000,immutable, to improve page reloading performance at development time. Once cached, these requests will never reach the development server again. If different versions are installed (as reflected in the lockfile of the package manager), the additional version Query (v= XXX) automatically invalidates them.

Next look at the source code is how to achieve the above function

The source code

When the local server starts, it is prebuilt

const server = await createServer({
    root,
    base: options.base,
    mode: options.mode,
    configFile: options.config,
    logLevel: options.logLevel,
    clearScreen: options.clearScreen,
    server: cleanOptions(options),
})

await server.listen()
Copy the code

After the server object is created by createServer, the server.listen method is called to start the server. When started, the httpServer.listen method is executed

When createServer is executed, the server.listen method is overridden

let isOptimized = false
// overwrite listen to run optimizer before server start
const listen = httpServer.listen.bind(httpServer)
httpServer.listen = (async (port: number. args:any[]) = > {if(! isOptimized) {try {
            // Call the buildStart hook function for all plug-ins
            await container.buildStart({})
            await runOptimize()
            isOptimized = true
        } catch (e) {
            httpServer.emit('error', e)
            return}}returnlisten(port, ... args) })as any
Copy the code

The buildStart method for all plug-ins is called first, followed by the runOptimize method

const runOptimize = async() = > {// Get the cache path, the default is node_modules/.vite
    if (config.cacheDir) {
        // Indicates that a prebuild is currently underway
        server._isRunningOptimizer = true
        try {
            server._optimizeDepsMetadata = await optimizeDeps(config)
        } finally {
            server._isRunningOptimizer = false
        }
        server._registerMissingImport = createMissingImporterRegisterFn(server)
    }
}
Copy the code

The code above call optimizeDeps method first, then call createMissingImporterRegisterFn method.

Let’s take a look at the optimizeDeps method, which is a big one, and let’s take a step-by-step look at what it does

export async function optimizeDeps(
    config: ResolvedConfig,
    force = config.server.force, // Set to true forces dependency prebuild
    asCommand = false, newDeps? : Record<string.string>, // missing imports encountered after server has startedssr? :boolean
) :Promise<DepOptimizationMetadata | null> {
    // reassign configconfig = { ... config,command: 'build',}const { root, logger, cacheDir } = config

    // Splice _metadata.json file path (usually in node_modules/.vite/_metadata.json)
    const dataPath = path.join(cacheDir, '_metadata.json')
    // Generate the hash value from the lockfile, viet.config. js fields of the package manager
    // The package.json dependencies list will also be used, but this version does not include dependencies
    const mainHash = getDepHash(root, config)
    const data: DepOptimizationMetadata = {
        hash: mainHash,
        browserHash: mainHash,
        optimized: {},}// ...
Copy the code

Json file, usually in node_modules/.vite/_metadata.json. The hash value is then generated using getDepHash. And create a data object

The _metadata.json file stores information about pre-built modules, as described later

if(! force) {let prevData: DepOptimizationMetadata | undefined
    try {
        // Get the contents of the _metadata.json file in cacheDir
        prevData = JSON.parse(fs.readFileSync(dataPath, 'utf-8'))}catch (e) {}
    // If _metadata.json has content and the previous hash value is the same as the hash value just generated
    // Returns the current _metadata.json content without any dependency changes
    if (prevData && prevData.hash === data.hash) {
        log('Hash is consistent. Skipping. Use --force to override.')
        return prevData
    }
}
Copy the code

If force is not set, read the contents of the last file from _metadata.json and check whether the hash value is the same as the current one. If so, return the contents of _metadata.json.

The logic then is that if the dependency changes, or is not pre-built; Create an empty folder according to cacheDir, in this case a.vite folder. Then create package.json file in the folder and say “type”: “module”

// If there is cacheDir (default.vite), clear the cache folder
// If not, create an empty file
if (fs.existsSync(cacheDir)) {
    emptyDir(cacheDir)
} else {
    // Return the first directory path created if recursive is true, or undefined if not
    fs.mkdirSync(cacheDir, { recursive: true})}// create package.json in cacheDir and say "type": "module"
// Function: Indicates to Node that all files in the cache directory should be identified as ES modules
writeFile(
    path.resolve(cacheDir, 'package.json'),
    JSON.stringify({ type: 'module'}))Copy the code

summary

Here is a summary of the above process

  • Generate hash values based on lockfile, viet.config. js related fields of the package manager
  • To obtain_metadata.jsonThe path to the file containing information about the last pre-built module
  • If not mandatory prebuild, compare_metadata.jsonHash in the file and the newly created hash value
    • Returns if consistent_metadata.jsonThe contents of the
    • If inconsistent, create/clear the cache folder (default is.vite); Created in cache filepackage.jsonFile and write to "type": "module"

Further down, determine if there is a dependency list based on the newDeps passed in. If not, collect the list using the scanImports method

let deps: Record<string.string>, missing: Record<string.string>
if(! newDeps) { ; ({ deps, missing } =await scanImports(config))
} else {
    deps = newDeps
    missing = {}
}
Copy the code

Automatic dependent search

The scanImports method is defined as follows, also step by step

export async function scanImports(config: ResolvedConfig) :Promise<{
    deps: Record<string.string>
    missing: Record<string.string>
}> {
    const start = performance.now()

    let entries: string[] = []
    // The default is index.html
    // By default, Vite will grab your index.html to detect dependencies that need to be pre-built. If you specify the build. RollupOptions. Input, Vite, in turn, to go to grab the entry point.
    // If neither of these are suitable for your needs, you can use this option to specify a custom entry, or an array of schemas relative to the root of the Vite project. This will override the default item inference.
    const explicitEntryPatterns = config.optimizeDeps.entries
    constbuildInput = config.build.rollupOptions? .inputif (explicitEntryPatterns) {
        / / if the configuration config. OptimizeDeps. Entries
        // Find the corresponding file in explicitEntryPatterns under config.root and return to the absolute path
        entries = await globEntries(explicitEntryPatterns, config)
    } else if (buildInput) {
        / / if the configuration build. RollupOptions. Input
        const resolvePath = (p: string) = > path.resolve(config.root, p)
        // The following logic changes the path of buildInput to the path relative to config.root
        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 {
        // Find the HTML file
        entries = await globEntries('**/*.html', config)
    }

    // Unsupported entry file types and virtual files should not scan for dependencies.
    // Filter non-.jsx.tx.mjs.html.vue.svelte. astro files, and the files must exist
    entries = entries.filter(
        (entry) = >
            (JS_TYPES_RE.test(entry) || htmlTypesRE.test(entry)) &&
            fs.existsSync(entry)
    )
Copy the code

First look for the entry file

  • If you haveconfig.optimizeDeps.entriesConfiguration item, the entry file is looked up from here
  • If you haveconfig.build.rollupOptions.inputConfiguration item, the entry file is looked up from here
  • None of the above, look under projects and directorieshtmlfile
const deps: Record<string.string> = {}
const missing: Record<string.string> = {}
// Create a plug-in container
const container = await createPluginContainer(config)
// Create the esbuildScanPlugin
const plugin = esbuildScanPlugin(config, container, deps, missing, entries)
// Get prebuilt plugins and configuration items
const{ plugins = [], ... esbuildOptions } = config.optimizeDeps? .esbuildOptions ?? {}Copy the code

Next is defined to find pre-built modules need to variables, such as container container, esbuildScanPlugin plugin, config. OptimizeDeps. EsbuildOptions defined in the plugins and other ESbuild configuration items

// Package each entry file and merge the incoming JS files together
await Promise.all(
entries.map((entry) = >
  build({
    absWorkingDir: process.cwd(),
    write: false.entryPoints: [entry],
    bundle: true.format: 'esm'.logLevel: 'error'.plugins: [...plugins, plugin], ... esbuildOptions }) ) )return {
deps,
missing
}
Copy the code

The next step is to call ESbuild to build the entire project from the entry module, get the modules that need to be pre-built, and return the list of dependencies. The esbuildScanPlugin plugin, which is the core of the search, is executed. Take a look at the implementation of the plugin

const plugin = esbuildScanPlugin(config, container, deps, missing, entries)

function esbuildScanPlugin(
    config: ResolvedConfig,
    container: PluginContainer,
    depImports: Record<string.string>,
    missing: Record<string.string>,
    entries: string[]
) :Plugin {
    const seen = new Map<string.string | undefined> ()// Run the container. ResolveId command to process the ID and return the absolute path of the module
     // Add this module path to the SEEN. Key is the parent directory of id + importer and value is the module absolute path
    const resolve = async (id: string, importer? :string) = > {}constinclude = config.optimizeDeps? .include// Ignore the package
    const exclude = [
        ...(config.optimizeDeps?.exclude || []),
        '@vite/client'.'@vite/env',]// Sets the return value of the build.onResolve hook function
    const externalUnlessEntry = ({ path }: { path: string }) = > ({
        path, // Module path
        // If entries contain the current ID, return false
        // If external is true, the current module will not be packaged into the bundle
        // This code means that if the current module is contained in entries, package it into a bundle
        external: !entries.includes(path),
    })

    return {
        name: 'vite:dep-scan'.setup(build){},}}Copy the code

The esbuildScanPlugin method returns a plug-in object; Defines a resolve function to find a path and to get a list of configuration items that need to be prebuilt and ignored

The plugin does different things for different types of files

External files,data:Initial file, CSS, JSON, unknown file

setup(build) {
    // External HTTP (s) files are not packaged into the bundle
    build.onResolve({ filter: externalRE }, ({ path }) = > ({
        path,
        external: true,}))// If it starts with data:, do not package it into the bundle
    build.onResolve({ filter: dataUrlRE }, ({ path }) = > ({
        path,
        external: true,}))// css & json
    build.onResolve(
        {filter: /\.(css|less|sass|scss|styl|stylus|pcss|postcss|json)$/},
        externalUnlessEntry
    )

    // known asset types
    build.onResolve(
        {filter: new RegExp((` \ \.${KNOWN_ASSET_TYPES.join('|')}) $`)},
        externalUnlessEntry
    )

    // known vite query types: ? worker, ? raw
    build.onResolve({ filter: SPECIAL_QUERY_RE }, ({ path }) = > ({
        path,
        external: true.// Do not inject into Boundle}}))Copy the code

Third-party libraries

These are relatively simple, so I don’t need to talk about them here. Now let’s look at how to deal with third party dependencies

build.onResolve({ filter: /^[\w@][^:]/ }, async ({ path: id, importer }) => {
    // Check whether the imported third-party module is included in exclude
    if(exclude? .some((e) = > e === id || id.startsWith(e + '/'))) {
        return externalUnlessEntry({ path: id })
    }
    // If the current module is already collected
    if (depImports[id]) {
        return externalUnlessEntry({ path: id })
    }
    // Obtain the absolute path of the third-party module
    const resolved = await resolve(id, importer)
    if (resolved) {
        // Virtual path, non-absolute path, non-.jsx.txx.mjs.html.vue.svelte. astro files return true
        if (shouldExternalizeDep(resolved, id)) {
            return externalUnlessEntry({ path: id })
        }
        // key !!!! Third party dependencies are collected here
        // If the path contains the node_modules substring, or the file exists in the include
        if (resolved.includes('node_modules') || include? .includes(id)) {// OPTIMIZABLE_ENTRY_RE = /\\.(? :m? js|ts)$/
            if (OPTIMIZABLE_ENTRY_RE.test(resolved)) {
                // Add to depImports
                depImports[id] = resolved
            }
            // If the current ID, such as vue, is not included in entries, do not package the file into the bundle; package it instead
            return externalUnlessEntry({ path: id })
        } else {
            const namespace = htmlTypesRE.test(resolved) ? 'html' : undefined
            // linked package, keep crawling
            return {
                path: path.resolve(resolved),
                namespace,
            }
        }
    } else {
        // The module corresponding to the ID is not found
        missing[id] = normalizePath(importer)
    }
})
Copy the code

If the module import path starts with a letter, number, underscore, Chinese character, or @, it will be caught by the hook function. Obtain the module absolute path; Add the module to depImports if the module path contains the node_modules substring, or if the module exists in include and is an MJS, JS, or TS file

If the id passed in is not resolved to the module path, add it to missing

HTML, Vue files

For HTML and Vue files, set the namespace to HTML

// htmlTypesRE = /\.(html|vue|svelte|astro)$/
Importer: the absolute path from which the file is imported
// Set the path and set the namespace to HTML
build.onResolve(
    { filter: htmlTypesRE },
    async ({ path, importer }) => {
        return {
            path: await resolve(path, importer)
            namespace: 'html',}})Copy the code

When the build.onLoad hook function is executed, the namespace for HTML hits an onLoad hook function

build.onLoad({ filter: htmlTypesRE, namespace: 'html' }, async ({ path }) => {
    // Read HTML, Vue content
    let raw = fs.readFileSync(path, 'utf-8')
    // Replace the comment content with <! ---->
    raw = raw.replace(commentRE, '<! -- -- -- - > ')
    // True if.html ends
    const isHtml = path.endsWith('.html')
    // If it ends in.html, regex matches a script tag with type module, and vice versa, such as Vue matches a script tag with no type attribute
    // scriptModuleRE[1]: 
    // scriptModuleRE[2], scriptRE[1]: The contents of the script tag
    // scriptRE[1]: 
    const regex = isHtml ? scriptModuleRE : scriptRE
    / / reset the regex lastIndex
    regex.lastIndex = 0
    let js = ' '
    let loader: Loader = 'js'
    let match: RegExpExecArray | null
    while ((match = regex.exec(raw))) {
        const [, openTag, content] = match
        // Get the SRC content of the start tag
        const srcMatch = openTag.match(srcRE)
        // Get the type content of the start tag
        const typeMatch = openTag.match(typeRE)
        // Get the content of lang on the start tag
        const langMatch = openTag.match(langRE)
        const type = typeMatch && (typeMatch[1] || typeMatch[2] || typeMatch[3])
        const lang = langMatch && (langMatch[1] || langMatch[2] || langMatch[3])
        // skip type="application/ld+json" and other non-JS types
        if ( type && !(type.includes('javascript') | |type.includes('ecmascript') | |type= = ='module')) {
            continue
        }
        // Set different loaders for different files
        if (lang === 'ts' || lang === 'tsx' || lang === 'jsx') {
            loader = lang
        }
        // Add the SRC or script block content to the js string
        if (srcMatch) {
            const src = srcMatch[1] || srcMatch[2] || srcMatch[3]
            js += `import The ${JSON.stringify(src)}\n`
        } else if (content.trim()) {
            js += content + '\n'}}// Empty the multi-line comment and single-line comment content
    const code = js.replace(multilineCommentsRE, '/ * * /).replace(singlelineCommentsRE, ' ')
    // Process Vue files in the form of TS + 
    if (loader.startsWith('ts') && (path.endsWith('.svelte') || (path.endsWith('.vue') && /<script\s+setup/.test(raw))) ) {
        // For Vue files in the form of TS + 
        // The solution is to add 'import 'x' for each import to force ESbuild to keep crawling
        while((m = importsRE.exec(code)) ! =null) {
            if (m.index === importsRE.lastIndex) {
                importsRE.lastIndex++
            }
            js += `\nimport ${m[1]}`}}// ...

    return {
        loader,
        contents: js,
    }
})
Copy the code

The hook function assembles all imports into a string and sets the loader property value. This is then handed over to ESbuild, with the intention that ESbuild will load the imports and collect the eligible modules.

HTML file: get all script tags in HTML. For those with SRC attributes, concatenate them into strings by import(). For inline script tags, concatenate the inline code into a string, which may contain import code, and return loader and Content

Vue files: Similar to HTML files, get the imported content and return the Loader and content

JSX, TSX, MJS

Check if the jsxInject option is configured and if it is added to the code, using the onload hook function

build.onLoad({ filter: JS_TYPES_RE }, ({ path: id }) = > {
    let ext = path.extname(id).slice(1)
    if (ext === 'mjs') ext = 'js'

    let contents = fs.readFileSync(id, 'utf-8')
    // If it is TSX, JSX and jsxInject is configured, inject jsxInject into the code
    if (ext.endsWith('x') && config.esbuild && config.esbuild.jsxInject) {
        contents = config.esbuild.jsxInject + `\n` + contents
    }
    return {
        loader: ext as Loader,
        contents,
    }
})
Copy the code

esbuildScanPluginPlug-in summary

The esbuildScanPlugin plugin collects modules to be pre-built. Vite will start with the entry file (default is index.html) and grab the source through ESbuild and automatically look for imported dependencies (i.e. “bare import”, meaning expected parsing from node_modules)

Back in the scanImports method, compile the source through ESbuild and collect the third-party modules that need to be pre-built. Finally return DEPS (collected modules) and MISSING (missing modules)

  // Package each entry file and merge the incoming JS files together
  await Promise.all(
    entries.map((entry) = >
      build({/ *... * /})))return {
    deps,
    missing
  }
Copy the code

The data structure of DEPS and MISSING is as follows

Deps: {import module name/path: absolute path after parsing} missing: {import module name/path: path to import this module module}Copy the code

Back in optimizeDeps, after calling the scanImports method, we get the missing modules and the list of modules that need to be pre-built.

let deps: Record<string.string>, missing: Record<string.string>
if(! newDeps) { ; ({ deps, missing } =await scanImports(config))
} else {
  deps = newDeps
  missing = {}
}
Copy the code

To continue down

// update browser hash
// This property is the v parameter on the url of the pre-built module
data.browserHash = createHash('sha256')
    .update(data.hash + JSON.stringify(deps))
    .digest('hex')
    .substr(0.8)
    
const missingIds = Object.keys(missing)
if (missingIds.length) {
    throw new Error(/ *... * /)}// Collect the remaining modules that need to be pre-built in include
/ / will config. OptimizeDeps? Files in.include are written to deps
constinclude = config.optimizeDeps? .includeif (include) {/ *... * /}

const qualifiedIds = Object.keys(deps)
// If deps does not exist, write to _metadata.json
if(! qualifiedIds.length) { writeFile(dataPath,JSON.stringify(data, null.2))
    log(`No dependencies to bundle. Skipping.\n\n\n`)
    return data
}
Copy the code

Update browserHash and throw an exception if any modules are missing. Whereas the config. OptimizeDeps. Include the remaining need to write deps pre-built modules. Determine if there are any dependencies that need to be pre-built, if data is not written to _metadata.json.

At this point, the collection process ends, and the compilation process begins

Rely on the collection summary

Collect third-party dependencies by compiling from the entry file (index.html) with ESbuild. An HTML or Vue file triggers the build.onload hook function, which retrieves the script tag. For those with SRC attributes, concatenate them to a string using import(); For inline script tags, concatenate the inline code into a string; Finally returns this string; This allows you to get the imported content of HTML and Vue files. After compiling, add the remaining modules in includes that need to be pre-built to the pre-built list. This pre-build collection phase ends and the compilation process begins.

The build process

Get Esbuild configuration items and initialize variables

You define a bunch of variables and get the Esbuild configuration items

// The number of pre-built modules required
const total = qualifiedIds.length
const maxListed = 5
const listed = Math.min(total, maxListed)
const flatIdDeps: Record<string.string> = {}
const idToExports: Record<string, ExportsData> = {}
const flatIdToExports: Record<string, ExportsData> = {}

const{ plugins = [], ... esbuildOptions } = config.optimizeDeps? .esbuildOptions ?? {}// Initialize es-module-lexer
await init
Copy the code

collect

It then iterates through the list of all required pre-built modules

for (const id in deps) {
    // replace > with __, \ and. With _
    const flatId = flattenId(id)
    const filePath = (flatIdDeps[flatId] = deps[id])
    // Read the code that requires pre-built modules
    const entryContent = fs.readFileSync(filePath, 'utf-8')
    let exportsData: ExportsData
    try {
        // Get the import and export locations from es-module-lexer
        exportsData = parse(entryContent) as ExportsData
    } catch {/ *... * /}
    for (const { ss, se } of exportsData[0]) {
        // Get the import content
        Exp = import {initCustomFormatter, warn} from '@vue/runtime-dom'
        const exp = entryContent.slice(ss, se)
        if (/export\s+\*\s+from/.test(exp)) {
            // set hasReExports to true if exp is export * from XXX
            exportsData.hasReExports = true
        }
    }
    idToExports[id] = exportsData
    flatIdToExports[flatId] = exportsData
}
Copy the code

Iterate through the list of all required pre-built modules and add the absolute path of corresponding modules to flatIdDeps; Read the module code, convert the module to AST via ES-module-lexer, and assign the value to exportsData. Exportsdata. hasReExports set to true. Finally, assign AST to idToExports and flatIdToExports

The structure of flatIdToExports, idToExports and flatIdDeps is as follows

# id: import module or import path
# flatId: replace > with __ and \ and. With _ import paths/modulesFlatIdDeps: {flatId: absolute path of module} idToExports: {id: AST of module, array} flatIdToExports: {flatIdDeps: {flatId: AST of module, array}Copy the code

Begin to build

Go ahead and start building the module through ESbuild

// The string to replace during the build process
const define: Record<string.string> = {
    'process.env.NODE_ENV': JSON.stringify(config.mode),
}
// Set the contents of esbuild.define to replace the compiled content
for (const key in config.define) {
    const value = config.define[key]
    define[key] = typeof value === 'string' ? value : JSON.stringify(value)
}

// Package the files in deps
const result = await build({
    absWorkingDir: process.cwd(),
    entryPoints: Object.keys(flatIdDeps),
    bundle: true.// This is true to convert ESM dependencies with many internal modules into a single module
    format: 'esm'.target: config.build.target || undefined.external: config.optimizeDeps? .exclude,logLevel: 'error'.splitting: true.sourcemap: true.outdir: cacheDir,
    ignoreAnnotations: true.metafile: true,
    define,
    plugins: [
        ...plugins,
        esbuildDepPlugin(flatIdDeps, flatIdToExports, config, ssr), // Notice here
    ],
    ...esbuildOptions,
})
Copy the code

All modules that need to be precompiled are compiled through ESbuild, and the entry files are the modules that need to be precompiled. It uses a custom plugin, esbuildDepPlugin, which I’ll examine below

Generate prebuilt module information

// Get the dependency graph generated by the packaged files
const meta = result.metafile!

// Gets the path of cacheDir relative to the working directory
const cacheDirOutputPath = path.relative(process.cwd(), cacheDir)
// Concatenate data and write data to _metadata.json
for (const id in deps) {
  const entry = deps[id]
  data.optimized[id] = {
    file: normalizePath(path.resolve(cacheDir, flattenId(id) + '.js')),
    src: entry,
    needsInterop: needsInterop( NeedsInterop is used to determine if this module is a CommonJS module
      id,
      idToExports[id],
      meta.outputs,
      cacheDirOutputPath
    )
  }
}
writeFile(dataPath, JSON.stringify(data, null.2))
/ / return data
return data
Copy the code

Get the dependency graph, concatenate the data object, write it to _metadata.json, and return ‘data’

  • For CommonJS modules, if ESbuild is setformat: 'esm', causing it to be wrapped as an ESM. It looks like this
// a.ts
module.exports = {
    test: 1
}
/ / the compiled
import { __commonJS } from "./chunk-Z47AEMLX.js";

// src/a.ts
var require_a = __commonJS({
  "src/a.ts"(exports.module) {
    module.exports = { test: 1}; }});// dep:__src_a_ts
var src_a_ts_default = require_a();
export {
  src_a_ts_default as default
};
//# sourceMappingURL=__src_a_ts.js.map
Copy the code
  • es-module-lexerConvert CommonJS module, converted content, exported and imported are empty arrays

_metadata.jsonintroduce

Assuming only Vue is introduced in the project, the resulting _metadata.json looks like this

{
  "hash": "861d0c42"."browserHash": "c30d2c95"."optimized": {
    "vue": {
      "file": "/xxx/node_modules/.vite/vue.js".// Prebuild the generated address
      "src": "/xxx/node_modules/vue/dist/vue.runtime.esm-bundler.js".// Source code address
      "needsInterop": false // Is the CommonJS module converted into an ESM module}}}Copy the code

esbuildDepPlugin

The esbuildDepPlugin plugin is defined as follows

export function esbuildDepPlugin(
    qualified: Record<string.string>,
    exportsData: Record<string, ExportsData>, config: ResolvedConfig, ssr? :boolean
) :Plugin {
    // Create the ESM pathfinder function
    const _resolve = config.createResolver({ asSrc: false })
    // Create the CommonJS pathfinder function
    const _resolveRequire = config.createResolver({
        asSrc: false.isRequire: true,})const resolve = (
        id: string.// Current file
        importer: string.// The absolute path to import the file
        kind: ImportKind, // Import typeresolveDir? :string) :Promise<string | undefined> = > {let _importer: string
        if (resolveDir) {
            _importer = normalizePath(path.join(resolveDir, The '*'))}else {
            Importer indicates the file that imports this file
            // If the importer exists in qualified set the corresponding file path, otherwise set the importer
            _importer = importer in qualified ? qualified[importer] : importer
        }
        const resolver = kind.startsWith('require')? _resolveRequire : _resolve// Return different pathfinder functions according to different module types
        return resolver(id, _importer, undefined, ssr)
    }

    return {
        name: 'vite:dep-pre-bundle'.setup(build){},}}Copy the code

The esbuildDepPlugin function creates a function that returns a different pathfinder function based on the module type; And returns a plug-in object

The main functions and hook functions of this plug-in object are as follows

// qualified contains all entry modules
// If flatId is in the entry module, set namespace to dep
function resolveEntry(id: string) {
    const flatId = flattenId(id)
    if (flatId in qualified) {
        return {
            path: flatId,
            namespace: 'dep',}}}// Block the bare module
build.onResolve(
    { filter: /^[\w@][^:]/ },
    async ({ path: id, importer, kind }) => {
        let entry: { path: string; namespace: string } | undefined if (! importer) {// If there is no importer, it is an importer file
            // Call the resolveEntry method, which returns a value if any
            if ((entry = resolveEntry(id))) return entry
            // The entry file may have an alias. After removing the alias, call the resolveEntry method
            const aliased = await _resolve(id, undefined.true)
            if (aliased && (entry = resolveEntry(aliased))) {
                return entry
            }
        }

        // use vite's own resolver
        const resolved = await resolve(id, importer, kind)
        if (resolved) {
            // ...
            
            // HTTP (s) path
            if (isExternalUrl(resolved)) {
                return {
                    path: resolved,
                    external: true,}}return {
                path: path.resolve(resolved)
            }
        }
    }
)
Copy the code

The hook function does this

  • Set a namespace for the pre-built module entry fileSet todep`
  • HTTP (s) type paths are not packaged into bundles and remain unchanged
  • Other types only return paths

There is also a build.onLoad hook function for the entry file, which reads as follows

const root = path.resolve(config.root)
build.onLoad({ filter: /. * /.namespace: 'dep' }, ({ path: id }) => {
    // Get the absolute path corresponding to the id
    const entryFile = qualified[id]
    // Get the path of id relative to root
    let relativePath = normalizePath(path.relative(root, entryFile))
    // Splice paths
    if (
        !relativePath.startsWith('/') &&
        !relativePath.startsWith('.. / ') && relativePath ! = ='. '
    ) {
        relativePath = `. /${relativePath}`
    }

    let contents = ' '
    const data = exportsData[id]
    // Get import and export information
    const [imports, exports] = data
    if(! imports.length && !exports.length) {
        // cjs
        contents += `export default require("${relativePath}"); `
    } else {
        if (exports.includes('default')) {
            contents += `import d from "${relativePath}"; export default d; `
        }
        if (
            data.hasReExports ||
            exports.length > 1 ||
            exports[0]! = ='default'
        ) {
            contents += `\nexport * from "${relativePath}"`}}let ext = path.extname(entryFile).slice(1)
    if (ext === 'mjs') ext = 'js'
    return {
        loader: ext as Loader,
        contents,
        resolveDir: root,
    }
})
Copy the code

The hook function builds a virtual module and imports the pre-built entry module. The content of the virtual module is as follows

  • CommonJS type file, exported virtual module content isExport default require(" module path ");
  • export defaultThe exported virtual module content isImport d from "module path "; export default d;
  • For other ESM files, the exported virtual module content isExport * from "module path"

This virtual module is then used to start packaging all pre-rendered modules.

Summary of pre-built modules

  • Iterate over all pre-built modules, adding the absolute path of the corresponding module toflatIdDeps; Read the module code, passes-module-lexerConvert the module to an AST and assign a value toexportsData. To find out ifexport * from xxxForm the code, if anyexportsData.hasReExportsSet totrue. Finally, assign the AST toidToExportsandflatIdToExports
  • Package all pre-built modules through ESbuild. And set thebundlefortrue. This implements the above transformation of ESM dependencies with many internal modules into a single module
  • Finally, the pre-built module information is generated

This completes the pre-build. Back to the runOptimize method

  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

The value returned after the prebuild is mounted to server._OptimizeDEPsMetadata.

How do I register new dependent precompiled functions

Create a function that through createMissingImporterRegisterFn and mount this function on to the server. The _registerMissingImport. This function registers new dependency precompilations

export function createMissingImporterRegisterFn(
    server: ViteDevServer
) : (id: string, resolved: string, ssr? :boolean) = >void {
    letknownOptimized = server._optimizeDepsMetadata! .optimizedlet currentMissing: Record<string.string> = {}
    let handle: NodeJS.Timeout

    let pendingResolve: (() = > void) | null = null

    async function rerun(ssr: boolean | undefined) {}

    return function registerMissingImport(}}Copy the code

The return function registerMissingImport is called when a new module needs to be precompiled

return function registerMissingImport(
id: string,
 resolved: string, ssr? :boolean
) {
  if(! knownOptimized[id]) {// Collect modules that need to be precompiled
    currentMissing[id] = resolved
    if (handle) clearTimeout(handle)
    handle = setTimeout(() = > rerun(ssr), debounceMs)
    server._pendingReload = new Promise((r) = > {
      pendingResolve = r
    })
  }
}
Copy the code

Function to add the modules to be precompiled to currentMissing, and then call rerun

async function rerun(ssr: boolean | undefined) {
  // Get the new precompiled module
  const newDeps = currentMissing
  currentMissing = {}
  // Merge old and new precompiled modules
  for (const id in knownOptimized) {
    newDeps[id] = knownOptimized[id].src
  }
  try {
    server._isRunningOptimizer = true
    server._optimizeDepsMetadata = null
    // Call optimizeDeps to begin the precompilation process
    const newData = (server._optimizeDepsMetadata = await optimizeDeps(
      server.config,
      true.// Note that the value is true, indicating that the cache needs to be cleared and precompiled again
      false,
      newDeps, // newDeps is passed
      ssr
    ))
    // Update the list of precompiled modulesknownOptimized = newData! .optimized }catch (e) {
  } finally {
    server._isRunningOptimizer = false
    pendingResolve && pendingResolve()
    server._pendingReload = pendingResolve = null
  }
  // Clear the transformResult property of all modules
  server.moduleGraph.invalidateAll()
  // Notify the client to reload the page
  server.ws.send({
    type: 'full-reload'.path: The '*'})},Copy the code

The above code combines the old and new precompiled modules and then calls the optimizeDeps function to rebuild all the modules. The important point to note is that force is passed true to clear the cache and recompile. NewDeps is also passed in, and instead of collecting the pre-built list again, the newDeps passed in is used directly

if(! newDeps) { ; ({ deps, missing } =await scanImports(config))
} else {
  deps = newDeps
  missing = {}
}
Copy the code

When the new prebuild is complete, notify the client to reload the page.

summary

The whole precompilation process is as follows

How is the import path mapped to the cache directory

When a prebuilt module is imported into the requested module, the preAliasPlugin gets and returns the prebuilt path when overwriting the import path.

Take a look at this logic in the preAliasPlugin plugin

// Inside the resolveId of the preAliasPlugin
resolveId(id, importer, _, ssr) {
  if(! ssr && bareImportRE.test(id)) {returntryOptimizedResolve(id, server, importer); }}Copy the code

Call the tryOptimizedResolve method

/ / tryOptimizedResolve inside
const cacheDir = server.config.cacheDir
const depData = server._optimizeDepsMetadata

if(! cacheDir || ! depData)return

const getOptimizedUrl = (optimizedData: typeof depData.optimized[string]) = > {
  return (
    optimizedData.file +
    `? v=${depData.browserHash}${
    optimizedData.needsInterop ? `&es-interop` : ` `
    }`)}// check if id has been optimized
const isOptimized = depData.optimized[id]
if (isOptimized) {
  return getOptimizedUrl(isOptimized)
}
Copy the code

As you can see, get the path of the cache file from the prebuilt list based on the id passed in, and concatenate the V parameter; For modules converted from CommonJS to ESM, an ES-Interop parameter is concatenated. Analyzing the importAnalysis plug-in shows that the import logic is overridden at the import location for urls with es-Interop parameters. This explains why CommonJS modules can also be introduced via ESM.