Configuration Resolution
Whether in development or production, configuration file resolution is necessary. The resolution process mainly consists of vite.config
configuration resolution, plugins
sorting and initialization (executing each plugin's config hook
), and loading .env
files (none by default).
Loading the vite.config
Module
let { configFile } = config;
if (configFile !== false) {
const loadResult = await loadConfigFromFile(
configEnv,
configFile,
config.root,
config.logLevel
);
if (loadResult) {
config = mergeConfig(loadResult.config, config);
configFile = loadResult.path;
configFileDependencies = loadResult.dependencies;
}
}
By default, config
is inlineConfig
const inlineConfig = {
root,
base: options.base,
mode: options.mode,
configFile: options.config,
logLevel: options.logLevel,
clearScreen: options.clearScreen,
optimizeDeps: { force: options.force },
build: buildOptions
};
options
are configurations made by users through commands in the console. For example, the configFile
configuration item's value comes from the user executing vite --config=xxx
, which is undefined
by default. Therefore, the conditional statement will execute by default unless vite --config=false
is executed.
In the loadConfigFromFile
function, the following work is mainly done:
- Get the path of
vite.config
in the file system
The suffix of vite.config
should actually have 6
types, including all suffixes of js
and ts
under both ESM
and CJS
const DEFAULT_CONFIG_FILES = [
'vite.config.js',
'vite.config.mjs',
'vite.config.ts',
'vite.config.cjs',
'vite.config.mts',
'vite.config.cts'
];
The priority of searching for configuration files in the file system decreases from top to bottom. The default configuration module of Vite
(cannot be modified
) is in the project's root directory, so the default search will only check if it exists in the root directory, searching from top to bottom until it finds the project's configuration file.
Determine if the configuration module is an
ESM
module or aCJS
moduleDetermining which module the configuration module belongs to is simple, usually by directly checking the configuration file suffix. For special suffixes, you can check if
type: module
is set in the project'spackage.json
module.jslet isESM = false; if (/\.m[jt]s$/.test(resolvedPath)) { isESM = true; } else if (/\.c[jt]s$/.test(resolvedPath)) { isESM = false; } else { try { const pkg = lookupFile(configRoot, ['package.json']); isESM = !!pkg && JSON.parse(pkg).type === 'module'; } catch (e) {} }
Another point to note is that the search for
package.json
is implemented through thelookupFile
method. The implementation logic is to start searching from theconfigRoot
directory, and if not found, continue searching in the parent directory until thepackage.json
module is found. If not found, it returnsundefined
.Load different modules in different ways
This step is quite interesting, as the parsing solutions for ESM
and CJS
are quite different. However, both rely on esbuild
to execute the build process.
Why execute the build process?
Actually, configuration modules are the same as regular modules, they may depend on other modules. But you might think that since we're in the runtime phase, we could directly load the configuration file through
import
(esm) orrequire
(cjs), without needing to bundle first. My consideration goes back to the meaning of bundling, which in simple terms is container compatibility and optimizing project loading process (including reducing package size, split chunk, etc.). Here, it should be for compatibility reasons, as there might be module dependencies between different specifications, which could lead to module resolution errors. Another reason is to optimize parsing speed.Node
's module resolution process (demo) can be simply summarized asload module -> compile module -> execute module -> load module -> ...
and so on recursively resolving various modules. This process will inevitably be time-consuming for large-scale dependencies. If we bundle first, then fornode
it only needsload module -> compile module -> execute module
, or even justcompile module -> execute module
. Combined withesbuild
as a native build tool, the bundling speed is very fast, so overall it will improve the module resolution process.Esbuild
Bundling ProcessIn the bundling process,
Vite
injects two plugins:externalize-deps
andinject-file-scope-variables
. The purpose of the former plugin is to filter out modules that don't use relative paths (e.g.,import react from '@vitejs/plugin-react'
,import { defineConfig } from '/demo'
); the purpose of the latter plugin is to inject three path constants related to modules (__dirname
,__filename
,__vite_injected_original_import_meta_url
) for non-filtered modules. The code implementation is as follows:jsasync function bundleConfigFile(fileName, isESM = false) { const importMetaUrlVarName = '__vite_injected_original_import_meta_url'; const result = await build$3({ // ... other configuration items omitted format: isESM ? 'esm' : 'cjs', plugins: [ { name: 'externalize-deps', setup(build) { build.onResolve({ filter: /.*/ }, args => { const id = args.path; if (id[0] !== '.' && !path$o.isAbsolute(id)) { return { external: true }; } }); } }, { name: 'inject-file-scope-variables', setup(build) { build.onLoad({ filter: /\.[cm]?[jt]s$/ }, async args => { const contents = await fs$l.promises.readFile( args.path, 'utf8' ); const injectValues = `const __dirname = ${JSON.stringify(path$o.dirname(args.path))};` + `const __filename = ${JSON.stringify(args.path)};` + `const ${importMetaUrlVarName} = ${JSON.stringify(pathToFileURL(args.path).href)};`; return { loader: isTS(args.path) ? 'ts' : 'js', contents: injectValues + contents }; }); } } ] }); const { text } = result.outputFiles[0]; return { code: text, dependencies: result.metafile ? Object.keys(result.metafile.inputs) : [] }; }
ESM
Module ProcessingWrite the bundled configuration module to the file system with a
.mjs
suffix, then dynamically load the module information throughimport
, and finally remove the.mjs
suffix configuration module from the file system. The code implementation is as follows:jsconst dynamicImport = new Function('file', 'return import(file)'); if (isESM) { if (isTS(resolvedPath)) { fs$l.writeFileSync(resolvedPath + '.mjs', bundled.code); try { userConfig = (await dynamicImport(`${fileUrl}.mjs?t=${Date.now()}`)) .default; } finally { fs$l.unlinkSync(resolvedPath + '.mjs'); } } else { userConfig = (await dynamicImport(`${fileUrl}?t=${Date.now()}`)) .default; } }
CJS
Module ProcessingTo allow
require
to directly execute the compilation process without needing to execute the loading process, rewrite therequire.extensions['.js']
method to allow the bundled configuration module to be directly compiled. The code implementation is as follows:jsasync function loadConfigFromBundledFile(fileName, bundledCode) { const realFileName = fs$l.realpathSync(fileName); const defaultLoader = _require.extensions['.js']; _require.extensions['.js'] = (module, filename) => { if (filename === realFileName) { module._compile(bundledCode, filename); } else { defaultLoader(module, filename); } }; delete _require.cache[_require.resolve(fileName)]; const raw = _require(fileName); _require.extensions['.js'] = defaultLoader; return raw.__esModule ? raw.default : raw; } if (!userConfig) { const bundled = await bundleConfigFile(resolvedPath); dependencies = bundled.dependencies; userConfig = await loadConfigFromBundledFile( resolvedPath, bundled.code ); }
Merging Configuration Modules
The overall idea is that
vite.config
is the basic configuration module. Traverse the variable values ininlineConfig
(non-undefined
and non-null
), and merge the values into the corresponding properties ofvite.config
. The merging details code is as follows:jsfunction arraify(target) { return Array.isArray(target) ? target : [target]; } function isObject$2(value) { return Object.prototype.toString.call(value) === '[object Object]'; } // Since the execution order of alias is top-down, the order should be reversed here, meaning that items sorted later have higher priority. function mergeAlias(a, b) { if (!a) return b; if (!b) return a; if (isObject$2(a) && isObject$2(b)) { return { ...a, ...b }; } return [...normalizeAlias(b), ...normalizeAlias(a)]; } function mergeConfigRecursively(defaults, overrides, rootPath) { const merged = { ...defaults }; for (const key in overrides) { const value = overrides[key]; if (value == null) { continue; } const existing = merged[key]; if (existing == null) { merged[key] = value; continue; } if (key === 'alias' && (rootPath === 'resolve' || rootPath === '')) { merged[key] = mergeAlias(existing, value); continue; } else if (key === 'assetsInclude' && rootPath === '') { merged[key] = [].concat(existing, value); continue; } else if ( key === 'noExternal' && rootPath === 'ssr' && (existing === true || value === true) ) { merged[key] = true; continue; } if (Array.isArray(existing) || Array.isArray(value)) { merged[key] = [...arraify(existing ?? []), ...arraify(value ?? [])]; continue; } if (isObject$2(existing) && isObject$2(value)) { merged[key] = mergeConfigRecursively( existing, value, rootPath ? `${rootPath}.${key}` : key ); continue; } merged[key] = value; } return merged; }
Note that through the bundling process, we get the current module's dependencies (including the module itself) dependencies
, which will be related to the subsequent HMR handleHMRUpdate
update.
Plugin Initialization and Sorting
In Vite
, plugins are mainly divided into two types: user-written plugins and Vite
built-in plugins. For user-written plugins, they are divided into user plugin
and worker plugin
. The former will be called for entry modules using this.option.entry
, import()
, this.emitChunk
, etc.; while the latter is only for worker
modules. The calling process is as follows:
import type { ResolvedConfig } from 'vite';
import type { OutputChunk } from 'rollup';
const postfixRE = /[?#].*$/;
function cleanUrl(url: string): string {
return url.replace(postfixRE, '');
}
export async function bundleWorkerEntry(
config: ResolvedConfig,
id: string,
query: Record<string, string> | null
): Promise<OutputChunk> {
// bundle the file as entry to support imports
const { rollup } = await import('rollup');
const { plugins, rollupOptions, format } = config.worker;
const bundle = await rollup({
...rollupOptions,
input: cleanUrl(id),
plugins,
onwarn(warning, warn) {
onRollupWarning(warning, warn, config);
},
preserveEntrySignatures: false
});
// ...
}
It can be seen that for worker
module processing, it will directly call rollup
to generate the module, and the plugins
used here are the worker plugins
mentioned above. The processing timing of worker plugins
is before user normal plugins
. For plugin details, you can jump to worker plugin.
Initialize Plugins
Flatten all
plugins
in thevite.config
module, which meansVite
supportsasynchronous operations
andone plugin can export multiple plugins
in plugin implementation.jsasync function asyncFlatten(arr) { do { arr = (await Promise.all(arr)).flat(Infinity); } while (arr.some(v => v?.then)); return arr; }
Filter out plugins that don't need to be enabled. If a plugin has an
apply
property, determine whether the current plugin needs to be used based on the value of theapply
property. The code is as follows:jsconst rawUserPlugins = ( await asyncFlatten(config.plugins || []) ).filter(p => { if (!p) { // Filter out plugins that don't exist or are determined to be unnecessary after promise async execution return false; } else if (!p.apply) { // By default, the current plugin needs to be used return true; } else if (typeof p.apply === 'function') { // Execute the apply function in the plugin to determine whether to use the current plugin based on the function's return value. return p.apply({ ...config, mode }, configEnv); } else { // Plugin is compatible with the environment. return p.apply === command; } });
Sort Plugins, Determine Plugin Priority
Sort plugins according to their priority. The code is as follows:
jsfunction sortUserPlugins(plugins) { const prePlugins = []; const postPlugins = []; const normalPlugins = []; if (plugins) { plugins.flat().forEach(p => { if (p.enforce === 'pre') prePlugins.push(p); else if (p.enforce === 'post') postPlugins.push(p); else normalPlugins.push(p); }); } return [prePlugins, normalPlugins, postPlugins]; }
From the code, we can understand that
Vite
determines priority by configuring theenforce
property in the plugin, that is, determining the execution order based on theenforce
value ofpre
,post
, andrelative position of plugin configuration
.Execute and merge the
config
hook of user configuration plugins in the above sorting order. The execution of this hook also marks the final determination ofvite
configuration information (which can be modified by users).jsconst userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins]; for (const p of userPlugins) { if (p.config) { const res = await p.config(config, configEnv); if (res) { config = mergeConfig(config, res); } } }
Resolve Plugins
Loading env
Files
In vite.config
, you can define all variables in process.env
and envFiles
modules that start with prefixes
by configuring envPrefix
(default is [VITE_
]). Vite
divides env
module paths into the following 4
categories.
const envFiles = [
/** mode local file */ `.env.${mode}.local`,
/** mode file */ `.env.${mode}`,
/** local file */ '.env.local',
/** default file */ '.env'
];
The search method starts from the root directory by default (you can modify the default path by setting the envDir
property in the vite.config
configuration module). If not found, it continues searching in the parent directory until the env
module path is found. The search method is similar to the package.json
search method.
Loading env
modules will use dotenv
's capabilities for parsing. However, it should be noted that env
modules will not be injected into process.env
.
// let environment variables use each other
main({
parsed,
// Avoid affecting process.env
ignoreProcessEnv: true
});
After loading the env
module, a JS Object
will be obtained, and then all non-empty key-value pairs starting with prefixes
will be extracted.
function loadEnv(mode, envDir, prefixes = 'VITE_') {
if (mode === 'local') {
throw new Error(
'"local" cannot be used as a mode name because it conflicts with ' +
'the .local postfix for .env files.'
);
}
prefixes = arraify(prefixes);
const env = {};
const envFiles = [
/** mode local file */ `.env.${mode}.local`,
/** mode file */ `.env.${mode}`,
/** local file */ '.env.local',
/** default file */ '.env'
];
// check if there are actual env variables starting with VITE_*
// these are typically provided inline and should be prioritized
for (const key in process.env) {
if (
prefixes.some(prefix => key.startsWith(prefix)) &&
env[key] === undefined
) {
env[key] = process.env[key];
}
}
for (const file of envFiles) {
const path = lookupFile(envDir, [file], {
pathOnly: true,
rootDir: envDir
});
if (path) {
const parsed = main$1.exports.parse(fs$l.readFileSync(path), {
debug: process.env.DEBUG?.includes('vite:dotenv') || undefined
});
// let environment variables use each other
main({
parsed,
// prevent process.env mutation
ignoreProcessEnv: true
});
// only keys that start with prefix are exposed to client
for (const [key, value] of Object.entries(parsed)) {
if (
prefixes.some(prefix => key.startsWith(prefix)) &&
env[key] === undefined
) {
env[key] = value;
} else if (
key === 'NODE_ENV' &&
process.env.VITE_USER_NODE_ENV === undefined
) {
// NODE_ENV override in .env file
process.env.VITE_USER_NODE_ENV = value;
}
}
}
}
return env;
}
The final parsed env
is used as userEnv
. Users can finally access it through config.env
.
async function resolveConfig(
inlineConfig,
command,
defaultMode = 'development'
) {
// ...
const resolved = {
// ...
env: {
...userEnv,
BASE_URL,
MODE: mode,
DEV: !isProduction,
PROD: isProduction
}
// ...
};
//...
return resolved;
}
Differences Between Development and Production Environments
Both development and production environments will execute the resolveConfig
process
async function doBuild(inlineConfig = {}) {
const config = await resolveConfig(inlineConfig, 'build', 'production');
// ...
}
async function createServer(inlineConfig = {}) {
const config = await resolveConfig(inlineConfig, 'serve', 'development');
// ...
}
From the parameters passed in, we can clearly see that the second and third parameters are different. In the resolveConfig
function, there is no additional logic processing for different modes, only some logic to confirm the mode.
async function loadConfigFromFile(
configEnv,
configFile,
configRoot = process.cwd(),
logLevel
) {
// ...
const config = await (typeof userConfig === 'function'
? userConfig(configEnv)
: userConfig);
// ...
return {
path: normalizePath$3(resolvedPath),
config,
dependencies
};
}
async function resolveConfig(
inlineConfig,
command,
defaultMode = 'development'
) {
// Usually fallback
let mode = inlineConfig.mode || defaultMode;
if (mode === 'production') {
process.env.NODE_ENV = 'production';
}
const configEnv = {
mode,
command
};
if (configFile !== false) {
const loadResult = await loadConfigFromFile(
configEnv,
configFile,
config.root,
config.logLevel
);
if (loadResult) {
config = mergeConfig(loadResult.config, config);
configFile = loadResult.path;
configFileDependencies = loadResult.dependencies;
}
}
mode = inlineConfig.mode || config.mode || mode;
configEnv.mode = mode;
// Plugin resolution
const rawUserPlugins = (await asyncFlatten(config.plugins || [])).filter(
p => {
if (!p) {
return false;
} else if (!p.apply) {
return true;
} else if (typeof p.apply === 'function') {
return p.apply({ ...config, mode }, configEnv);
} else {
// Plugin execution is determined by mode
return p.apply === command;
}
}
);
for (const p of userPlugins) {
if (p.config) {
const res = await p.config(config, configEnv);
if (res) {
config = mergeConfig(config, res);
}
}
}
config = mergeConfig(config, externalConfigCompat(config, configEnv));
}
/**
* When legacy.buildRollupPluginCommonjs configuration is disabled, support rollupOptions.external. This function provides additional configuration support for config?.build?.rollupOptions?.external.
* */
function externalConfigCompat(config, { command }) {
// Only affects the build command
if (command !== 'build') {
return {};
}
const external = config?.build?.rollupOptions?.external;
// Skip if not configured
if (!external) {
return {};
}
let normalizedExternal = external;
if (typeof external === 'string') {
normalizedExternal = [external];
}
const additionalConfig = {
optimizeDeps: {
exclude: normalizedExternal,
esbuildOptions: {
plugins: [esbuildCjsExternalPlugin(normalizedExternal)]
}
}
};
return additionalConfig;
}