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 a- CJSmodule- Determining 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's- package.jsonmodule.js- let 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 the- lookupFilemethod. The implementation logic is to start searching from the- configRootdirectory, and if not found, continue searching in the parent directory until the- package.jsonmodule is found. If not found, it returns- undefined.
- 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) or- require(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 as- load 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 for- nodeit only needs- load module -> compile module -> execute module, or even just- compile module -> execute module. Combined with- esbuildas a native build tool, the bundling speed is very fast, so overall it will improve the module resolution process.
- EsbuildBundling Process- In the bundling process, - Viteinjects two plugins:- externalize-depsand- inject-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:js- async 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 Processing- Write the bundled configuration module to the file system with a - .mjssuffix, then dynamically load the module information through- import, and finally remove the- .mjssuffix configuration module from the file system. The code implementation is as follows:js- const 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 Processing- To allow - requireto directly execute the compilation process without needing to execute the loading process, rewrite the- require.extensions['.js']method to allow the bundled configuration module to be directly compiled. The code implementation is as follows:js- async 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 in- inlineConfig(non-- undefinedand non-- null), and merge the values into the corresponding properties of- vite.config. The merging details code is as follows:js- function 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 the- vite.configmodule, which means- Vitesupports- asynchronous operationsand- one plugin can export multiple pluginsin plugin implementation.js- async 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 the- applyproperty. The code is as follows:js- const 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: js- function 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 the- enforceproperty in the plugin, that is, determining the execution order based on the- enforcevalue of- pre,- post, and- relative 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 of- viteconfiguration information (which can be modified by users).js- const 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;
}