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.json
In thedependencies
The list of- Lockfile for package manager, for example
package-lock.json
.yarn.lock
Or,pnpm-lock.yaml
- May be in
vite.config.js
The 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.json
The path to the file containing information about the last pre-built module - If not mandatory prebuild, compare
_metadata.json
Hash in the file and the newly created hash value- Returns if consistent
_metadata.json
The contents of the - If inconsistent, create/clear the cache folder (default is
.vite
); Created in cache filepackage.json
File and write to"type": "module"
- Returns if consistent
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 have
config.optimizeDeps.entries
Configuration item, the entry file is looked up from here - If you have
config.build.rollupOptions.input
Configuration item, the entry file is looked up from here - None of the above, look under projects and directories
html
file
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
esbuildScanPlugin
Plug-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 set
format: '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-lexer
Convert CommonJS module, converted content, exported and imported are empty arrays
_metadata.json
introduce
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 file
Set to
dep` - 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 is
Export default require(" module path ");
export default
The exported virtual module content isImport d from "module path "; export default d;
- For other ESM files, the exported virtual module content is
Export * 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 to
flatIdDeps
; Read the module code, passes-module-lexer
Convert the module to an AST and assign a value toexportsData
. To find out ifexport * from xxx
Form the code, if anyexportsData.hasReExports
Set totrue
. Finally, assign the AST toidToExports
andflatIdToExports
- Package all pre-built modules through ESbuild. And set the
bundle
fortrue
. 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.