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.configin 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
ESMmodule or aCJSmoduleDetermining 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: moduleis set in the project'spackage.jsonmodule.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.jsonis implemented through thelookupFilemethod. The implementation logic is to start searching from theconfigRootdirectory, and if not found, continue searching in the parent directory until thepackage.jsonmodule 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 fornodeit only needsload module -> compile module -> execute module, or even justcompile module -> execute module. Combined withesbuildas a native build tool, the bundling speed is very fast, so overall it will improve the module resolution process.EsbuildBundling ProcessIn the bundling process,
Viteinjects two plugins:externalize-depsandinject-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) : [] }; }ESMModule ProcessingWrite the bundled configuration module to the file system with a
.mjssuffix, then dynamically load the module information throughimport, and finally remove the.mjssuffix 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; } }CJSModule ProcessingTo allow
requireto 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.configis the basic configuration module. Traverse the variable values ininlineConfig(non-undefinedand 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
pluginsin thevite.configmodule, which meansVitesupportsasynchronous operationsandone plugin can export multiple pluginsin 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
applyproperty, determine whether the current plugin needs to be used based on the value of theapplyproperty. 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
Vitedetermines priority by configuring theenforceproperty in the plugin, that is, determining the execution order based on theenforcevalue ofpre,post, andrelative position of plugin configuration.Execute and merge the
confighook of user configuration plugins in the above sorting order. The execution of this hook also marks the final determination ofviteconfiguration 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;
}