本文旨在分析 vite 源码中解析 config 参数的函数 resolveConfig
config 来源
- inlineConfig:来自命令行或配置的
npm scripts
- vite.config.ts/vite.config.js:用户配置的文件
- Plugin.config:插件的 config 方法返回的配置项
涉及功能
- 设置
--configFile false
参数来禁用配置文件 - 按需加载插件
- 插件强制顺序
- 加载
.env
文件 plugin.config
钩子函数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. 设置 config
,mode
,configFileDependencies
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
转义读取,然后删除转义后的文件
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
参数:alias
、dedupe
这两个参数可以用于 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 的环境和模式:
- 命令行指定
- 配置文件
.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 函数
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
}
}
}