Make writing a habit together! This is the fourth day of my participation in the “Gold Digging Day New Plan · April More text Challenge”. Click here for more details.

Hello everyone, I am Xiao Yu. In the previous section, we saw the details of the process from typing the Vite command to finally getting the service up and running. In this section, we’ll start by looking at the logic of the entry configuration (the resolveConfig function).

As usual, we went to the DEMO, initialized a Vite project with the Vanilla template, and then created the vite.config.ts configuration file in the root directory as follows:

// vite.config.ts
import { defineConfig } from 'vite'
import vitePluginB from './plugins/vite-plugin-B'

export default defineConfig({
  server: {
    port: 8888
  },

  plugins: [{name: 'testA-plugin'.enforce: 'post'.config(config, configEnv) {
        console.log('Plugins A --> config', configEnv)
      },

      configResolved(resolvedConfigA) {
        console.log('Plug-in A --> configResolved')
      }
    },
    vitePluginB()
  ]
})

// ./plugins/vite-plugin-B
import type { Plugin } from 'vite'

export default function PluginB() :Plugin {
  return {
    name: 'testB-plugin'.enforce: 'pre'.config(config, configEnv) {
      console.log('Plugins B --> config', configEnv)
    },

    configResolved(resolvedConfigB) {
      console.log('Plug-in B --> configResolved')}}}Copy the code

DefineConfig specifies the HTTP server port 8888 and defines two plugins A and B. Both plugins use only config and configResolved hooks. Enforce is set to Pre for plugin B.

And then the entrance of the source (packages/vite/SRC/node/cli. Ts) breakpoint on:

Finally we execute the following command:

PNPX vite dev --host 0.0.0.0 --port 3333 --config vite.config.tsCopy the code

Forgetting the debugging process can be taught to fish, how to debug CLI code? Reconfigure the debugger environment.

Process analysis

Based on the above command, the following output is obtained:

options = {
  --: [],
  c: 'vite.config.ts'.config: 'vite.config.ts'.host:'0.0.0.0'.port: 3333,}Copy the code

{host: ‘0.0.0.0’, port: 3333} :

// dev
cli
  .command('[root]'.'start dev server') // default command
  .alias('serve') // the command is called 'serve' in Vite's API
  .alias('dev') // alias to align with the script name
  .option('--host [host]'.`[string] specify hostname`)
  .option('--port <port>'.`[number] specify port`)
  .option('--https'.`[boolean] use TLS + HTTP/2`)
  .option('--open [path]'.`[boolean | string] open browser on startup`)
  .option('--cors'.`[boolean] enable CORS`)
  .option('--strictPort'.`[boolean] exit if specified port is already in use`)
  .option(
    '--force'.`[boolean] force the optimizer to ignore the cache and re-bundle`
  )
  .action(async (root: string.options: ServerOptions & GlobalCLIOptions) => {
    // output structure is preserved even after bundling so require()
    // is ok here
    const { createServer } = await import('./server')
    try {
      // Action for dev and server
      const server = await createServer({
        root,								// undefined
        base: options.base,	// undefined
        mode: options.mode,	// undefined
        configFile: options.config,	// vite.config.ts
        logLevel: options.logLevel,	// undefined
        clearScreen: options.clearScreen,	// undefined
        server: cleanOptions(options)	// {host: '0.0.0.0', port: 3333}
      })
      
		 // ...
  })
Copy the code

What has Vite done since typing the order? As we already know, after entering a subcommand in the terminal, global parameters are filtered through cleanOptions and then HTTP server is created through createServer.

export async function createServer(
  inlineConfig: InlineConfig = {}
) :Promise<ViteDevServer> {
  // Get inlineConfig from the CLI and some global parameters
  // In dev mode, command is server and mode is development
  const config = await resolveConfig(inlineConfig, 'serve'.'development')
	// ...
}
Copy the code

Config is the core information relied upon throughout the createServer process. ResolveConfig function in the packages/vite/SRC/node/config. The ts, the function body is close to 400 lines of code, to understand the overall process, and then to read the source code. The overall process is shown in the figure below:

When resolveConfig is called, the inlineConfig parameter looks like the following:

{
  root: undefined.base: undefined.mode: undefined.configFile: 'vite.config.ts'.logLevel: undefined.clearScreen: undefined.server: { host: '0.0.0.0'.port: 3333}}Copy the code

For the entire Vite application, parameters are not only fetched from the command, but also loaded from the configuration file pointed to by configFile. Once the configuration is merged, the plug-in’s Config hook is called, and the hook argument is the complete Config information. Part of the configuration (shown in orange) is then normalized and the plug-in’s resolvedConfig hook is executed to complete the configuration parsing process.

Learn the whole resolveConfig process from the global view, and then learn the functional logic of green squares one by one. The logic for other colors is clearer, so just go back and read the internal details when you run into problems.

Load the configuration

// Parse the configuration process
export async function resolveConfig(
  inlineConfig: InlineConfig,
  command: 'build' | 'serve',
  defaultMode = 'development'
) :Promise<ResolvedConfig> {
  let config = inlineConfig
  let configFileDependencies: string[] = []
  // ...

  // This is the parameter we can get in the config hook
  const configEnv = {
    mode,
    command
  }
  
  // In Demo, configFile is vite.config.ts
  let { configFile } = config
  // In this example, configFile is vite.config.ts, and the loadConfigFromFile logic is entered
  if(configFile ! = =false) {
    const loadResult = await loadConfigFromFile(
      configEnv,
      configFile,
      config.root,
      config.logLevel
    )
    // Get the result of vite.confit.ts and merge the configuration. It can be seen that the CLI parameter has higher priority than the configuration file
    if (loadResult) {
      config = mergeConfig(loadResult.config, config)
      configFile = loadResult.path
      configFileDependencies = loadResult.dependencies
    }
  }
  // ...
}
Copy the code

The configEnv variable is first defined, with mode equal to development and command equal to server in dev mode. For this example, the configFile is vite.config.ts.

export async function loadConfigFromFile(configEnv: ConfigEnv, configFile? :string,
  configRoot: string = process.cwd(),	// The default value is the root path of the commandlogLevel? : LogLevel) :Promise<{
  path: string
  config: UserConfig
  dependencies: string[]} |null> {
  // Common logical execution time on node
  const start = performance.now()
  const getTime = () = > `${(performance.now() - start).toFixed(2)}ms`

  let resolvedPath: string | undefined
  let isTS = false
  let isESM = false
  let dependencies: string[] = []

  try {
    // Find package.json and change the isESM variable to true if there is type: module information
    const pkg = lookupFile(configRoot, ['package.json'])
    if (pkg && JSON.parse(pkg).type === 'module') {
      isESM = true}}catch (e) {}

  if (configFile) {
    resolvedPath = path.resolve(configFile)
    // Check whether the configuration file is a TS file
    isTS = configFile.endsWith('.ts')

    if (configFile.endsWith('.mjs')) {
      isESM = true}}else {
    MJS, vite.config.ts, vite.config. CJS, etc., so you can get the configuration in the file without specifying config
    const jsconfigFile = path.resolve(configRoot, 'vite.config.js')
    if (fs.existsSync(jsconfigFile)) {
      resolvedPath = jsconfigFile
    }

    if(! resolvedPath) {const mjsconfigFile = path.resolve(configRoot, 'vite.config.mjs')
      if (fs.existsSync(mjsconfigFile)) {
        resolvedPath = mjsconfigFile
        isESM = true}}if(! resolvedPath) {const tsconfigFile = path.resolve(configRoot, 'vite.config.ts')
      if (fs.existsSync(tsconfigFile)) {
        resolvedPath = tsconfigFile
        isTS = true}}if(! resolvedPath) {const cjsConfigFile = path.resolve(configRoot, 'vite.config.cjs')
      if (fs.existsSync(cjsConfigFile)) {
        resolvedPath = cjsConfigFile
        isESM = false}}}if(! resolvedPath) { debug('no config file found.')
    return null
  }

  try {
    let userConfig: UserConfigExport | undefined

    if (isESM) {
      / /... The example does not enter this logic, so it is omitted. Interested children can change viet.config. ts to viet.config.mjs
    }

    if(! userConfig) {// Build the configuration file with esbuild and output the CJS package
      const bundled = await bundleConfigFile(resolvedPath)
      // Get the built dependencies
      dependencies = bundled.dependencies
      // Use require.extensions to extend the support ts file to get the compiled results
      userConfig = await loadConfigFromBundledFile(resolvedPath, bundled.code)
      debug(`bundled config file loaded in ${getTime()}`)}// The result of the execution returns a promise. The configEnv parameter passed in is also why the configuration file supports both situational and asynchronous configuration.
    const config = await (typeof userConfig === 'function'
      ? userConfig(configEnv)
      : userConfig)
    
    return {
      path: normalizePath(resolvedPath),
      config,
      dependencies
    }
  } catch (e) {
   	// ...}}Copy the code

LoadConfigFromFile lookupFile(configRoot, [‘package.json’]) looks for package.json files from the current directory. If the current directory is not found, recursively look to the parent directory. Read the contents of the file and return. Identify the USE of the ESM module mechanism (isESM) by determining that type is equal to module in package.json. The isTS variable is then defined according to the configuration file suffix. If configFile information is not specified, Vite attempts to find possible configuration files such as Vite. Config. js, Vite. Config. MJS, Vite.

In this example the configuration file is viet.config. ts, so build ts with bundleConfigFile:

import { build } from 'esbuild'

async function bundleConfigFile(
  fileName: string,
  isESM = false
) :Promise<{ code: string; dependencies: string[] }> {
  const result = await build({
    // Work absolute path
    absWorkingDir: process.cwd(),
    / / the entry
    entryPoints: [fileName],
    // Output file
    outfile: 'out.js'.// Do not write to the file system
    write: false.// The build results run in Node
    platform: 'node'.// Build dependencies
    bundle: true.// Output format
    format: isESM ? 'esm' : 'cjs'./ / inline sourcemap
    sourcemap: 'inline'.// Outputs build information
    metafile: true.plugins: [
      // ..]})// Build the result
  const { text } = result.outputFiles[0]
  return {
    code: text,
    dependencies: result.metafile ? Object.keys(result.metafile.inputs) : []
  }
}
Copy the code

BundleConfigFile uses esbuild to build the viet.config. ts file and returns a list of products and dependencies as shown below:

Then through loadConfigFromBundledFile loading configuration:

// In this example, fileName is the configuration file passed in the absolute path /.. /vite.config.ts
// bundledCode is the result of the above esbuild
async function loadConfigFromBundledFile(
  fileName: string,
  bundledCode: string
) :Promise<UserConfig> {
  // Get the extension of the file, for example.ts
  const extension = path.extname(fileName)
  // The default.ts parser. Node does not support ts by default, so undefined
  const defaultLoader = require.extensions[extension]!
  // Extend the types supported by CJS require to enable Node to support TS parsing
  require.extensions[extension] = (module: NodeModule, filename: string) = > {
    if (filename === fileName) {
      // Call the module._compile method to compile code; (module as NodeModuleWithCompile)._compile(bundledCode, filename)
    } else {
      defaultLoader(module, filename)
    }
  }
  
  // If the service is restarted, there are cache results, so you need to clear the file cache
  delete require.cache[require.resolve(fileName)]
  const raw = require(fileName)
  const config = raw.__esModule ? raw.default : raw
  // reset the.ts extension definition, for example, to clear the ts parser
  require.extensions[extension] = defaultLoader
  return config
}
Copy the code

As you can see, node supports ts with require.extensions[extension]. Use (module as NodeModuleWithCompile)._compile(bundledCode, filename) to compile the loaded vite. Config. ts file. Finally return config as shown in the following figure:

The configuration defined in viet.config. ts is now all captured. In the case of the example, the final object is obtained from the configuration file, or if the result is a function, the configEnv object is passed and the function is executed to get the result. This is often used when options need to be decided based on the (dev/serve or build) command or a different mode.

MergeConfig: mergeConfig mergeConfig: mergeConfig: mergeConfig: mergeConfig: mergeConfig

/** * Merge two configurations **@export
 * @param {Record<string, any>} defaults
 * @param {Record<string, any>} overrides
 * @param {boolean} [isRoot=true]
 * @return {*}  {Record<string, any>}* /
export function mergeConfig(
  defaults: Record<string.any>,
  overrides: Record<string.any>,
  isRoot = true
) :Record<string.any> {
  // Merge isRoot with true as the default
  return mergeConfigRecursively(defaults, overrides, isRoot ? ' ' : '. ')}function mergeConfigRecursively(
  defaults: Record<string.any>,
  overrides: Record<string.any>,
  rootPath: string
) {
  // Make a shallow copy of the configuration obtained from the file vite.config.ts
  const merged: Record<string.any> = { ...defaults }
  
  // Iterate through the configuration specified on the CLI
  for (const key in overrides) {
    // If the CLI does not define an item, it does not overwrite the item and goes directly to the next round
    const value = overrides[key]
    if (value == null) {
      continue
    }

    // Obtain the configuration items corresponding to vite.config.ts
    const existing = merged[key]
    
    // If it is not defined in the configuration file, directly use the CLI configuration
    if (existing == null) {
      merged[key] = value
      continue
    }

    // fields that require special handling
    // rootPath is ""
    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' && existing === true) {
      continue
    }

    // There is an array
    if (Array.isArray(existing) || Array.isArray(value)) {
      merged[key] = [...arraify(existing ?? []), ...arraify(value ?? [])]
      continue
    }
    // All objects are recursive
    if (isObject(existing) && isObject(value)) {
      merged[key] = mergeConfigRecursively(
        existing,
        value,
        rootPath ? `${rootPath}.${key}` : key
      )
      continue
    }

    merged[key] = value
  }
  return merged
}
Copy the code

At this point, the entire configuration fetch and merge process is analyzed.

summary

Use a diagram to describe the process of loading configuration:

  1. Find package.json files in the current and parent directories and return the contents of the files.
  2. In this example, the configuration file is viet.config. ts, so the output CJS results will be built using esbuild;
  3. Extensions require.extensions[. Ts] to compile and load ts files with module._compile;
  4. Check whether the obtained config is a function. If so, pass in the configEnv function and get the result.
  5. Finally, merge the result of step 4 with the CLI parameters to obtain config.

To understand the whole process, if you were to implement a command line program that supports complex configuration, you could make configuration easier by following Vite in providing complete TypeScript type hints via defineConfig and then building with esBuild. Finally extend require.extensions to get the configuration.

Plugins and hooks

As we know, the Config and configResolved hooks of the plug-in are called during the resolveConfig phase. The execution order of the hooks depends on the Enforce property declared in the plug-in. Let’s take a look at the source code:

export async function resolveConfig(
  inlineConfig: InlineConfig,
  command: 'build' | 'serve',
  defaultMode = 'development'
) :Promise<ResolvedConfig> {
  let config = inlineConfig
  let configFileDependencies: string[] = []
  let mode = inlineConfig.mode || defaultMode
  
  // This is the parameter we can get in the config hook
  const configEnv = {
    mode,
    command
  }
  
	// ...
  mode = inlineConfig.mode || config.mode || mode
  configEnv.mode = mode

  // Get the list of plugins, flatten, and filter true plugins
  const rawUserPlugins = (config.plugins || []).flat().filter((p) = > {
    // Plug-ins with false values are ignored
    if(! p) {return false
    // If there is no apply attribute, it is an object and returns true
    } else if(! p.apply) {return true
    // Apply is a function, and configEnv is passed in to execute the plugin function
    } else if (typeof p.apply === 'function') {
      returnp.apply({ ... config, mode }, configEnv)// Finally, apply can define a property value, such as {apply: 'build'}, and use the plug-in only under build
    } else {
      return p.apply === command
    }
  }) as Plugin[]

  // Sort the plug-ins according to Enforce
  const [prePlugins, normalPlugins, postPlugins] =
    sortUserPlugins(rawUserPlugins)

	// ...

  // Execute the plugin's config hooks in turn
  const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins]
  for (const p of userPlugins) {
    if (p.config) {
      // If there is a return value in config, it is merged into config
      const res = await p.config(config, configEnv)
      if (res) {
        config = mergeConfig(config, res)
      }
    }
  }

	// ...
  const resolved: ResolvedConfig = {
    // ...
  }

  // Merge internal and user-defined plug-ins; (resolved.pluginsas Plugin[]) = await resolvePlugins(
    resolved,
    prePlugins,
    normalPlugins,
    postPlugins
  )

  // call configResolved hooks
  await Promise.all(userPlugins.map((p) = >p.configResolved? .(resolved)))return resolved
}
Copy the code

The code just leaves the processing of the plug-in as follows:

  1. Testb-plugin {apply: ‘build’} is used to filter plugins after plugins have been flattened.

  2. Sort the filtered plugins by sortUserPlugins:

    export function sortUserPlugins(
      plugins: (Plugin | Plugin[])[] | undefined
    ) :Plugin[].Plugin[].Plugin[]] {
      // Define an array of front plugins
      const prePlugins: Plugin[] = []
     	// Define a post-plug-in array
      const postPlugins: Plugin[] = []
      // There is no plugin array for defining the Enforce attribute
      const normalPlugins: Plugin[] = []
    
      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]
    }
    Copy the code

    The logic of this function is simple. We define prePlugins, postPlugins, normalPlugins, and store enforce: pre, Enforce: post, and any plugins without enforce. Finally, the list of plugins is returned in the order prePlugins, normalPlugins, postPlugins.

  3. Then execute the plug-in config hook in turn:

    // Execute the plugin's config hooks in turn
    const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins]
    for (const p of userPlugins) {
      if (p.config) {
        // If there is a return value in config, it is merged into config
        const res = await p.config(config, configEnv)
        if (res) {
          config = mergeConfig(config, res)
        }
      }
    }
    Copy the code

    Note two details. The config hook function can be a Promise, and mergeConfig is executed if config returns a value that means res is present. This mechanism allows you to pass the configuration of enforece: Pre to enforce: Post.

  4. Once the Config hook is executed, a set of configurations will be specified, which will be examined in the next section. Resolved variable to store all regulated configurations; Call resolvePlugins to merge built-in plug-ins and user-defined external plug-ins:

    ; (resolved.pluginsas Plugin[]) = await resolvePlugins(
      resolved,
      prePlugins,
      normalPlugins,
      postPlugins
    )
    
    export async function resolvePlugins(config: ResolvedConfig, prePlugins: Plugin[], normalPlugins: Plugin[], postPlugins: Plugin[]) :Promise<Plugin[] >{
      // Package the built scenario
      const isBuild = config.command === 'build'
    	// Add build configuration in build mode
      const buildPlugins = isBuild
        ? (await import('.. /build')).resolveBuildPlugins(config)
        : { pre: [].post: []}return [
        isBuild ? null : preAliasPlugin(),
        aliasPlugin({ entries: config.resolve.alias }), ... prePlugins, config.build.polyfillModulePreload ? modulePreloadPolyfillPlugin(config) :null, resolvePlugin({ ... config.resolve,root: config.root,
          isProduction: config.isProduction,
          isBuild,
          packageCache: config.packageCache,
          ssrConfig: config.ssr,
          asSrc: true}), htmlInlineProxyPlugin(config), cssPlugin(config), config.esbuild ! = =false ? esbuildPlugin(config.esbuild) : null,
        jsonPlugin(
          {
            namedExports: true. config.json }, isBuild ), wasmPlugin(config), webWorkerPlugin(config), workerImportMetaUrlPlugin(config), assetPlugin(config), ... normalPlugins, definePlugin(config), cssPostPlugin(config), config.build.ssr ? ssrRequireHookPlugin(config) :null. buildPlugins.pre, ... postPlugins, ... buildPlugins.post,// internal server-only plugins are always applied after everything else. (isBuild ? [] : [clientInjectionsPlugin(config), importAnalysisPlugin(config)]) ].filter(Boolean) as Plugin[]
    }
    Copy the code

    Inserting user-defined plug-ins into the entire array of plug-ins controls the order in which plug-ins are executed:

    • Alias
    • withenforce: 'pre'User plugins for
    • Vite core plug-in
    • User plug-in with no Enforce value
    • Plugin for Vite build
    • withenforce: 'post'User plugins for
    • Vite post-build plug-in (minimize, manifest, report)

    From resolvePlugins, you can see the complete list of plugins. The function of each plug-in is not explained here, so you can directly view the source code of the corresponding plug-in.

  5. After the specification configuration and integration plug-in is processed, the plug-in’s configResolved hook is called, which can be used to read and store the final resolved configuration.

    constresolved: ResolvedConfig = { ... config,// Config file
      configFile: configFile ? normalizePath(configFile) : undefined.// Dependencies in the configuration file
      configFileDependencies,
      // CLI incoming configuration
      inlineConfig,
      // Start root directory
      root: resolvedRoot,
      // Public base path
      base: BASE_URL,
      // File path resolution is relevant
      resolve: resolveOptions,
      // Static resources folder
      publicDir: resolvedPublicDir,
      // Cache directory
      cacheDir,
      // The command that is currently started
      command,
      / / mode
      mode,
      // Whether the production environment
      isProduction,
      // User plugins
      plugins: userPlugins,
      / / the server
      server,
      // Build the configuration
      build: resolvedBuildOptions,
      // Preview options
      preview: resolvePreviewOptions(config.preview, server),
      // Environment variables
      env: {
        ...userEnv,
        BASE_URL,
        MODE: mode,
        DEV: !isProduction,
        PROD: isProduction
      },
      // Additional static resources
      assetsInclude(file: string) {
        return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file)
      },
      // Log handler you
      logger,
      packageCache: new Map(),
      // Vite provides the parser
      createResolver,
      // Precompile the configuration
      optimizeDeps: {
        ...config.optimizeDeps,
        esbuildOptions: {
          keepNames: config.optimizeDeps? .keepNames,preserveSymlinks: config.resolve? .preserveSymlinks, ... config.optimizeDeps? .esbuildOptions } },// Worker-related configuration
      worker: resolvedWorkerOptions
    }
    
    await Promise.all(userPlugins.map((p) = >p.configResolved? .(resolved)))Copy the code
  6. After executing the configResolved hook, return to Resolved and take a look at the resulting configuration in the picture:

The figure above shows a list of custom and default plug-ins;

Finally, complete configuration details;

summary

In this section, we analyzed that when parsing the configuration, the plugin will sort the configuration according to Enforce and output pre, Normal, and POST. The plug-in then executes the Config and configResolved hooks, which fire just after the configuration has been resolved and merged and whose return value can be passed to the next component, and the configResolved hooks, which fire after all configuration specifications and internal and external plug-ins have been merged.

After this section covers the processing of plug-ins, let’s move on to the processing of other configurations.

Standard configuration

In this section we look at common configuration alias and environment variable (ENV) handling.

alias

// eslint-disable-next-line node/no-missing-require
export const CLIENT_ENTRY = require.resolve('vite/dist/client/client.mjs')
// eslint-disable-next-line node/no-missing-require
export const ENV_ENTRY = require.resolve('vite/dist/client/env.mjs')

// Alias of the client code
const clientAlias = [
  { find: ^ / / / /? @vite\/env/, replacement: () = > ENV_ENTRY },
  { find: ^ / / / /? @vite\/client/, replacement: () = > CLIENT_ENTRY }
]

// Merge internal and user-defined alias configurations
const resolvedAlias = normalizeAlias(
  mergeAlias(
    // @ts-ignore because @rollup/plugin-alias' type doesn't allow function
    // replacement, but its implementation does work with function values.clientAlias, config.resolve? .alias || config.alias || [] ) )Copy the code

One detail you can see is that the Vite alias configuration can be placed under config, but this is a deprecated usage.

export interface UserConfig {
  /**
   * Import aliases
   * @deprecated use `resolve.alias` instead
   */alias? : AliasOptions; }Copy the code

When we design our own tools, we will adjust the configuration position and parameters due to the function iteration. In this case, we can kindly use warning messages, API comments @deprecated and other methods to gradually let users transition to the new usage.

Merge built-in and user-defined aliases with MergeAliases:

function mergeAlias(
  a?: AliasOptions,
  b?: AliasOptions
) :AliasOptions | undefined {
  // if a does not exist, return b directly
  if(! a)return b
  // if b does not exist, return a
  if(! b)return a
  // a and b are objects in sequence
  if (isObject(a) && isObject(b)) {
    return{... a, ... b } }// the order is flipped because the alias is resolved from top-down,
  // where the later should have higher priority
  return [...normalizeAlias(b), ...normalizeAlias(a)]
}
Copy the code

The merge strategy is simple: if A does not exist, return B directly; If b does not exist, return a directly; If a and B are both objects, deconstruct them into one object, with A first and B second. NormalizeAlias = normalizeAlias = normalizeAlias = normalizeAlias = normalizeAlias

function normalizeAlias(o: AliasOptions = []) :Alias[] {
  // In the case of arrays, returns the result of a call to normalizeSingleAlias for each alias configuration
  NormalizeSingleAlias returns the result of a call to normalizeSingleAlias for all values corresponding to keys
  return Array.isArray(o)
    ? o.map(normalizeSingleAlias)
    : Object.keys(o).map((find) = >
        normalizeSingleAlias({
          find,
          replacement: (o as any)[find]
        })
      )
}

function normalizeSingleAlias({ find, replacement, customResolver }: Alias) :Alias {
  // If find is a string and both find and replacement end in /, the final/is removed
  if (
    typeof find === 'string' &&
    find.endsWith('/') &&
    replacement.endsWith('/')
  ) {
    find = find.slice(0, find.length - 1)
    replacement = replacement.slice(0, replacement.length - 1)}const alias: Alias = {
    find,
    replacement
  }
  if (customResolver) {
    alias.customResolver = customResolver
  }
  return alias
}
Copy the code

As you can see from the code above, the Alias attribute can be defined as an object or an array. NormalizeSingleAlias for an array, normalizeSingleAlias for an object, normalizeSingleAlias for a key and value.

NormalizeSingleAlias Determines whether find, replacement ends in /, and if so, deletes /. This rule comes from @rollup/plugin-alias. Then return alias.

env

 // If envDir is specified, get the environment variable from envDir
  const envDir = config.envDir
    ? normalizePath(path.resolve(resolvedRoot, config.envDir))
    : resolvedRoot
  constuserEnv = inlineConfig.envFile ! = =false &&
    loadEnv(mode, envDir, resolveEnvPrefix(config))
Copy the code

Environment variables first get the file path from the defined envDir configuration and normalizePath processing. The environment variable prefix is then resolved by resolveEnvPrefix:

export function resolveEnvPrefix({
  envPrefix = 'VITE_'
}: UserConfig) :string[] {
  envPrefix = arraify(envPrefix)
  // If you define an empty environment variable prefix, then you can directly fetch the environment variable of the entire process. This is a dangerous operation, so it gives a hint of sensitive information
  if (envPrefix.some((prefix) = > prefix === ' ')) {
    throw new Error(
      `envPrefix option contains value '', which could lead unexpected exposure of sensitive information.`)}return envPrefix
}
Copy the code

When we define envPrefix as null in the configuration, we can get the environment variables on the entire process, and therefore some sensitive information. So Vite will raise an Error to warn you of the dangerous configuration. After processing the prefix, we get the defined environment variable from the file:

export function loadEnv(
  mode: string,
  envDir: string,
  prefixes: string | string[] = 'VITE_'
) :Record<string.string> {
  // local is a built-in vite suffix, such as.env.development.local and.env.local
  if (mode === 'local') {
    throw new Error(
      `"local" cannot be used as a mode name because it conflicts with ` +
        `the .local postfix for .env files.`)}// Get the prefix of the variable
  prefixes = arraify(prefixes)
  const env: Record<string.string> = {}
  
  // The environment variable file is read by default
  const envFiles = [
    /** mode local file */ `.env.${mode}.local`./** mode file */ `.env.${mode}`./** local file */ `.env.local`./** default file */ `.env`
  ]

  // Some of the environment variables defined by CLI parameters with VITE prefixes can also be obtained from import.meta.env
  for (const key in process.env) {
    if (
      prefixes.some((prefix) = > key.startsWith(prefix)) &&
      env[key] === undefined
    ) {
      env[key] = process.env[key] as string}}// Walk through the environment variables file
  for (const file of envFiles) {
    const path = lookupFile(envDir, [file], true)
    if (path) {
      // Get the content of the file and parse it through Dotenv
      const parsed = dotenv.parse(fs.readFileSync(path), {
        debug: process.env.DEBUG? .includes('vite:dotenv') | |undefined
      })

      // let environment variables use each other
      dotenvExpand({
        parsed,
        // prevent process.env mutation
        ignoreProcessEnv: true
      } as any)

      // 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
}
Copy the code

LoadEnv does three things:

  • Local,.env.development,.env.local,.env.
  • Read the process’s environment variables and add them to the env if they have the correct prefix. This can be set when vite is started.
  • Then read the environment variable files in turn, using dotenv to parse and dotenV-expand to diffuse. Finally, the VITE prefix environment variables are cached in env.

The whole process of reading environment variables is over.

conclusion

This section analyzes how to obtain the configuration from the parameters and configuration file vite.config.ts after executing vite from the command. For ts configuration files, we will use esbuild to build TS files in CJS format, and then extend support for TS files through require.config. You can merge the configuration with CLI parameters to obtain the customized configuration.

We then sort the user-defined plug-ins according to the Enforce attribute of the plug-in and call the Config hook in turn. After all configurations are normalized, the configResolved hook fires.

Finally, it analyzes the processing process of configuring alias and env, and knows that alias is based on @rollup/plugins-alias, and env uses the power of dotenv and DotenV-expand packages to complete the setting of environment variables.