Skip to content
小 K. 同学

小 K. 同学

Author

本文旨在分析 vite 源码中解析 config 参数的函数 resolveConfig

config 来源

  1. inlineConfig:来自命令行或配置的 npm scripts
  2. vite.config.ts/vite.config.js:用户配置的文件
  3. Plugin.config:插件的 config 方法返回的配置项

涉及功能

  1. 设置 --configFile false 参数来禁用配置文件
  2. 按需加载插件
  3. 插件强制顺序
  4. 加载 .env 文件
  5. plugin.config 钩子函数
  6. plugin.configResolved 钩子函数

1. 入口

ts
// /src/node/config.ts
import { resolveConfig, InlineConfig, ResolvedConfig } from '../config';

const config = await resolveConfig(inlineConfig, 'serve', 'development');

2. 参数定义

ts
function resolveConfig(
  inlineConfig: InlineConfig,
  command: 'build' | 'serve',
  defaultMode = 'development',
): Promise<ResolvedConfig>;

3. 设置 configmodeconfigFileDependencies

ts
let config = inlineConfig; // 存储配置
let configFileDependencies: string[] = []; // 存储配置依赖
let mode = inlineConfig.mode || defaultMode; // 设置 mode

if (mode === 'production') {
  process.env.NODE_ENV = 'production';
}

const configEnv = {
  mode,
  command,
};

4. 加载配置文件,重置配置 mode

同时知道可以在命令行使用 --configFile false 配置来禁用读取配置文件

ts
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;
  }
}

mode = inlineConfig.mode || config.mode || mode;
configEnv.mode = mode;

loadConfigFromFile 就是根据项目目录,获取相关的配置文件,当使用配置文件类型是 ts 且使用 ES Module 时,会被 esbuild 转义读取,然后删除转义后的文件

code
ts
function loadConfigFromFile(
  configEnv: ConfigEnv,
  configFile?: string,
  configRoot: string = process.cwd(),
  logLevel?: LogLevel,
): Promise<{
  path: string;
  config: UserConfig;
  dependencies: string[];
} | null> {
  let resolvedPath: string | undefined; // 路径
  let isTS = false; // 是否是 ts
  let isESM = false; // 是否是 ES Module
  let dependencies: string[] = []; // 依赖

  // 检查 package.json 并检测类型,将 isESM 置为 true
  try {
    const pkg = lookupFile(configRoot, ['package.json']);
    if (pkg && JSON.parse(pkg).type === 'module') {
      isESM = true;
    }
  } catch (e) {}

  // 判定是否有 configFile 参数
  if (configFile) {
    resolvedPath = path.resolve(configFile);
    isTS = configFile.endsWith('.ts');

    if (configFile.endsWith('.mjs')) {
      isESM = true;
    }
  } else {
    // 依次检测 configRoot 路径下是否有以下配置文件(fs.existsSync):
    // vite.config.js
    // vite.config.mjs(存在则取其配置,并将 isESM = true)
    // vite.config.ts(存在则取其配置,并将 isESM = true)
    // vite.config.cjs(存在则取其配置,并将 isESM = false)
    // 按上面检测顺序优先级,取配置文件路径存储到 resolvedPath
    resolvedPath = path.resolve(configRoot, 'vite.config.[xx]');
    // 上面几个配置文件都没扫到,则直接返回 null
  }

  // 若均为取到配置文件的路径
  if (!resolvePath) {
    debug('no config file found.');
    return null;
  }

  let userConfig: UserConfigExport | undefined;

  if (isESM && isTS) {
    const fileUrl = require('url').pathToFileURL(resolvedPath);
    // esbuild 打包
    const bundled = await bundleConfigFile(resolvedPath, true);
    dependencies = bundled.dependencies;

    fs.writeFileSync(resolvedPath + '.js', bundled.code); // 暂存读取的配置
    userConfig = (await dynamicImport(`${fileUrl}.js?t=${Date.now()}`)).default;
    fs.unlinkSync(resolvedPath + '.js'); // 删除临时文件
  }
}

5. 解析应用插件

按需加载 plugin.apply 属性强制插件排序 plugin.enforce 属性执行 plugin.config 钩子函数,添加用户配置

ts
// resolve plugins
// 扁平数组,筛选应用在当前 command 下的插件
const rawUserPlugins = (config.plugins || []).flat().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 {
    return p.apply === command;
  }
}) as Plugin[];
// sortUserPlugins 方法根据插件的 enforce 参数进行排序:
// pre: Vite 核心插件之【前】调用
// 默认: Vite 核心插件之【后】调用
// post: Vite 核心插件之【后】调用
const [prePlugins, normalPlugins, postPlugins] =
  sortUserPlugins(rawUserPlugins);
// 执行 plugin.config 钩子函数,再次配置
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);
    }
  }
}

6. 解析 resolve 参数:aliasdedupe

这两个参数可以用于 resolve 同级,此处解析 /^[\/]?@vite\/env//^[\/]?@vite\/client/,是为了解析 hmr 的客户端文件路径,对 /@vite 路径请求开头的文件进行重定向

ts
const clientAlias = [
  /* vite package 目录由 `import.meta.url` 获取 */
  /* ENV_ENTRY 为 vite package 下的 `dist/client/env.mjs` 文件 */
  { find: /^[\/]?@vite\/env/, replacement: () => ENV_ENTRY },
  /* CLIENT_ENTRY 为 vite package 下的 `dist/client/client.mjs` 文件 */
  { find: /^[\/]?@vite\/client/, replacement: () => CLIENT_ENTRY },
];
const resolvedAlias = mergeAlias(
  clientAlias,
  config.resolve?.alias || config.alias || [],
);
const resolveOptions: ResolvedConfig['resolve'] = {
  dedupe: config.dedupe,
  ...config.resolve,
  alias: resolvedAlias,
};

7. 配置用户环境变量

加载 .env 文件 配置用户环境变量,官网 区分 pro/dev 环境和模式 也有体现。至此,用户有三次改变 pro/dev 的环境和模式:

  1. 命令行指定
  2. 配置文件
  3. .env 文件

而且,这里有通过 --envFile false 禁用加载 .env 文件,但可见上一篇说明 cli 并未配置这个 option

ts
const resolvedRoot = normalizePath(
  config.root ? path.resolve(config.root) : process.cwd(),
);

const envDir = config.envDir
  ? normalizePath(path.resolve(resolvedRoot, config.envDir))
  : resolvedRoot;

const userEnv =
  inlineConfig.envFile !== false &&
  loadEnv(mode, envDir, resolveEnvPrefix(config));

const isProduction = (process.env.VITE_USER_NODE_ENV || mode) === 'production';
if (isProduction) {
  // in case default mode was not production and is overwritten
  process.env.NODE_ENV = 'production';
}

loadEnv 方法就是根据 mode 使用 dotenv (npm pkg) 加载环境下的 .env 文件,并 判断 'VITE__' 前缀,同时根据 用户配置的 NODE_ENV 配置 VITE_USER_NODE_ENV 变量

ts
function loadEnv(
  mode: string,
  envDir: string,
  prefixes: string | string[] = 'VITE_',
): Record<string, string> {
  prefixes = arraify(prefixes); // string => string[]
  const env: Record<string, string> = {};
  const envFiles = [`.env.${mode}.local`, `.env.${mode}`, `.env.local`, `.env`];

  for (const key in process.env) {
    if (
      prefixes.some((prefix) => key.startsWith(prefix)) &&
      env[key] === undefined
    ) {
      env[key] = process.env[key] as string;
    }
  }

  for (const file of envFiles) {
    const path = lookupFile(envDir, [file], true);
    if (path) {
      const parsed = dotenv.parse(fs.readFileSync(path), {
        debug: !!process.env.DEBUG || 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') {
          // NODE_ENV override in .env file
          process.env.VITE_USER_NODE_ENV = value;
        }
      }
    }
  }
  return env;
}

8. 解析相关配置

ts
const BASE_URL = resolveBaseUrl(config.base, command === 'build', logger);
const resolvedBuildOptions = resolveBuildOptions(resolvedRoot, config.build);
// resolve cache directory
const pkgPath = lookupFile(resolvedRoot, [`package.json`], true /* pathOnly */);
const cacheDir = config.cacheDir
  ? path.resolve(resolvedRoot, config.cacheDir)
  : pkgPath && path.join(path.dirname(pkgPath), `node_modules/.vite`);

const assetsFilter = config.assetsInclude
  ? createFilter(config.assetsInclude)
  : () => false;

const { publicDir } = config;
const resolvedPublicDir =
  publicDir !== false && publicDir !== ''
    ? path.resolve(
        resolvedRoot,
        typeof publicDir === 'string' ? publicDir : 'public',
      )
    : '';

9. 添加内置插件

如 css 解析,ts 解析等,并对所有插件 排序

ts
(resolved.plugins as Plugin[]) = await resolvePlugins(
  resolved,
  prePlugins,
  normalPlugins,
  postPlugins,
);
// call configResolved hooks
await Promise.all(userPlugins.map((p) => p.configResolved?.(resolved)));

async function resolvePlugins(
  config: ResolvedConfig,
  prePlugins: Plugin[],
  normalPlugins: Plugin[],
  postPlugins: Plugin[],
): Promise<Plugin[]> {
  const isBuild = config.command === 'build';

  const buildPlugins = isBuild
    ? (await import('../build')).resolveBuildPlugins(config)
    : { pre: [], post: [] };

  return [
    /* 插件排序 */
    /* 详情可见下面的 【插件机制】 一文 */
  ].filter(Boolean) as Plugin[];
}

10. 创建一个内部使用的插件解析器,执行所有的插件

ts
// create an internal resolver to be used in special scenarios, e.g.
// optimizer & handling css @imports
const createResolver: ResolvedConfig['createResolver'] = (options) => {
  let aliasContainer: PluginContainer | undefined;
  let resolverContainer: PluginContainer | undefined;
  return async (id, importer, aliasOnly, ssr) => {
    let container: PluginContainer;
    // 创建 container
    return (await container.resolveId(id, importer, { ssr }))?.id;
  };
};

11. 执行 Hook 函数

plugin.configResolved

ts
await Promise.all(userPlugins.map((p) => p.configResolved?.(resolved)));

12. 汇总 resolved

这里有 用户 env 中额外的变量数据

ts
const resolved: ResolvedConfig = {
  ...config,
  configFile: configFile ? normalizePath(configFile) : undefined,
  configFileDependencies,
  inlineConfig,
  root: resolvedRoot,
  base: BASE_URL,
  resolve: resolveOptions,
  publicDir: resolvedPublicDir,
  cacheDir,
  command,
  mode,
  isProduction,
  plugins: userPlugins,
  server,
  build: resolvedBuildOptions,
  preview: resolvePreviewOptions(config.preview, server),
  env: {
    ...userEnv,
    BASE_URL,
    MODE: mode,
    DEV: !isProduction,
    PROD: isProduction,
  },
  assetsInclude(file: string) {
    return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file);
  },
  logger,
  packageCache: new Map(),
  createResolver,
  optimizeDeps: {
    ...config.optimizeDeps,
    esbuildOptions: {
      keepNames: config.optimizeDeps?.keepNames,
      preserveSymlinks: config.resolve?.preserveSymlinks,
      ...config.optimizeDeps?.esbuildOptions,
    },
  },
};

MIT