Vite 本体のコードを読む(build編)
モチベーション
仕事やプライベートで Vite を使うことが増えてきた。
なんとなく中で何が行われているのか気になってきたので本体のコードを見てみる。
最初から全てのコードを読むことはできないので、今回はビルドに絞っています。
前提
2023/12/31 時点の main のコードを見ていきます。
コードリーディング後の理解
ざっくりやってること
- 依存関係の最適化
- Rollup 用のパラメータを調整 & Rollup でビルドする時に調整したパラメータを渡す
環境構築は下記を参考にしたらいけた。
build 時に実行される処理を見つける
ざっと本体のコードを見ていたところ、いかにも build 時に実行されそうな関数を見つけた。
Vite をインストールしたプロジェクトの node_modules/vite 配下で上記の関数の中に console.log を仕込んで yarn build
したみたところ、ログが表示されたのであってそう。
テストコードもあった。
実行結果が知りたくなったら、このテストを実行するのが良さそう。
(パスにアンダースコアが含まれていて、そのままリンクを載せると Markdown の太字と認識されてしまうので、テストコードを見たい方は下記のURLをコピペしてください)
https://github.com/vitejs/vite/blob/main/packages/vite/src/node/__tests__/build.spec.ts
build 関数の中身を見ていく
ここから本題の build 関数の中身を見ていきます。
設定ファイルを解決する
resolveConfig
関数を使ってユーザーが指定した設定とデフォルトの設定をマージしている。
inlineConfig
はユーザーが指定した設定。
テストコードを実行した時に config の中身
{
root: '<作業ディレクトリ>/vite/packages/vite/src/node/__tests__/packages/build-project',
logLevel: 'silent',
build: {
target: [ 'es2020', 'edge88', 'firefox78', 'chrome87', 'safari14' ],
cssTarget: [ 'es2020', 'edge88', 'firefox78', 'chrome87', 'safari14' ],
outDir: 'dist',
assetsDir: 'assets',
assetsInlineLimit: 4096,
cssCodeSplit: true,
sourcemap: false,
rollupOptions: {},
minify: 'esbuild',
terserOptions: {},
write: false,
emptyOutDir: null,
copyPublicDir: true,
manifest: false,
lib: false,
ssr: false,
ssrManifest: false,
ssrEmitAssets: false,
reportCompressedSize: true,
chunkSizeWarningLimit: 500,
watch: null,
commonjsOptions: { include: [Array], extensions: [Array] },
dynamicImportVarsOptions: { warnOnError: true, exclude: [Array] },
modulePreload: { polyfill: true },
cssMinify: true
},
plugins: [
{
name: 'vite:build-metadata',
renderChunk: [AsyncFunction: renderChunk]
},
{
name: 'vite:watch-package-data',
buildStart: [Function: buildStart],
buildEnd: [Function: buildEnd],
watchChange: [Function: watchChange],
handleHotUpdate: [Function: handleHotUpdate]
},
{ name: 'vite:pre-alias', resolveId: [AsyncFunction: resolveId] },
{
name: 'alias',
buildStart: [AsyncFunction: buildStart],
resolveId: [Function: resolveId]
},
{
name: 'vite:modulepreload-polyfill',
resolveId: [Function: resolveId],
load: [Function: load]
},
{
name: 'vite:resolve',
resolveId: [AsyncFunction: resolveId],
load: [Function: load]
},
{
name: 'vite:html-inline-proxy',
resolveId: [Function: resolveId],
load: [Function: load]
},
{
name: 'vite:css',
configureServer: [Function: configureServer],
buildStart: [Function: buildStart],
transform: [AsyncFunction: transform]
},
{
name: 'vite:esbuild',
configureServer: [Function: configureServer],
buildEnd: [Function: buildEnd],
transform: [AsyncFunction: transform]
},
{ name: 'vite:json', transform: [Function: transform] },
{
name: 'vite:wasm-helper',
resolveId: [Function: resolveId],
load: [AsyncFunction: load]
},
{
name: 'vite:worker',
configureServer: [Function: configureServer],
buildStart: [Function: buildStart],
load: [Function: load],
shouldTransformCachedModule: [Function: shouldTransformCachedModule],
transform: [AsyncFunction: transform],
renderChunk: [Function: renderChunk],
generateBundle: [Function: generateBundle]
},
{
name: 'vite:asset',
buildStart: [Function: buildStart],
configureServer: [Function: configureServer],
resolveId: [Function: resolveId],
load: [AsyncFunction: load],
renderChunk: [Function: renderChunk],
generateBundle: [Function: generateBundle]
},
{
name: 'test',
resolveId: [Function: resolveId],
load: [Function: load]
},
{ name: 'vite:wasm-fallback', load: [AsyncFunction: load] },
{ name: 'vite:define', transform: [AsyncFunction: transform] },
{
name: 'vite:css-post',
renderStart: [Function: renderStart],
transform: [AsyncFunction: transform],
renderChunk: [AsyncFunction: renderChunk],
augmentChunkHash: [Function: augmentChunkHash],
generateBundle: [AsyncFunction: generateBundle]
},
{
name: 'vite:build-html',
transform: [AsyncFunction: transform],
generateBundle: [AsyncFunction: generateBundle]
},
{
name: 'vite:worker-import-meta-url',
shouldTransformCachedModule: [Function: shouldTransformCachedModule],
transform: [AsyncFunction: transform]
},
{
name: 'vite:asset-import-meta-url',
transform: [AsyncFunction: transform]
},
{
name: 'vite:force-systemjs-wrap-complete',
renderChunk: [Function: renderChunk]
},
{
name: 'commonjs',
version: '25.0.7',
options: [Function: options],
buildStart: [Function: buildStart],
buildEnd: [Function: buildEnd],
load: [Function: load],
shouldTransformCachedModule: [Function: shouldTransformCachedModule],
transform: [Function: transform]
},
{
name: 'vite:data-uri',
buildStart: [Function: buildStart],
resolveId: [Function: resolveId],
load: [Function: load]
},
{
name: 'vite:dynamic-import-vars',
resolveId: [Function: resolveId],
load: [Function: load],
transform: [AsyncFunction: transform]
},
{
name: 'vite:import-glob',
configureServer: [Function: configureServer],
transform: [AsyncFunction: transform]
},
{
name: 'vite:build-import-analysis',
resolveId: [Function: resolveId],
load: [Function: load],
transform: [AsyncFunction: transform],
renderChunk: [Function: renderChunk],
generateBundle: [Function: generateBundle]
},
{
name: 'vite:esbuild-transpile',
renderChunk: [AsyncFunction: renderChunk]
},
{
name: 'vite:terser',
renderChunk: [AsyncFunction: renderChunk],
closeBundle: [Function: closeBundle]
},
{
name: 'vite:reporter',
transform: [Function: transform],
options: [Function: options],
buildStart: [Function: buildStart],
buildEnd: [Function: buildEnd],
renderStart: [Function: renderStart],
renderChunk: [Function: renderChunk],
generateBundle: [Function: generateBundle],
writeBundle: [AsyncFunction: writeBundle],
closeBundle: [Function: closeBundle]
},
{ name: 'vite:load-fallback', load: [AsyncFunction: load] }
],
configFile: undefined,
configFileDependencies: [],
inlineConfig: {
root: '<作業ディレクトリ>/vite/packages/vite/src/node/__tests__/packages/build-project',
logLevel: 'silent',
build: { write: false },
plugins: [ [Object] ]
},
base: '/',
rawBase: '/',
resolve: {
mainFields: [ 'browser', 'module', 'jsnext:main', 'jsnext' ],
conditions: [],
extensions: [
'.mjs', '.js',
'.mts', '.ts',
'.jsx', '.tsx',
'.json'
],
dedupe: [],
preserveSymlinks: false,
alias: [ [Object], [Object] ]
},
publicDir: '<作業ディレクトリ>/vite/packages/vite/src/node/__tests__/packages/build-project/public',
cacheDir: '<作業ディレクトリ>/vite/packages/vite/node_modules/.vite',
command: 'build',
mode: 'production',
ssr: {
target: 'node',
optimizeDeps: { disabled: true, esbuildOptions: [Object] }
},
isWorker: false,
mainConfig: null,
isProduction: false,
css: { lightningcss: undefined },
esbuild: { jsxDev: true },
server: {
preTransformRequests: true,
sourcemapIgnoreList: [Function: isInNodeModules],
middlewareMode: false,
fs: {
strict: true,
allow: [Array],
deny: [Array],
cachedChecks: false
}
},
preview: {
port: undefined,
strictPort: undefined,
host: undefined,
https: undefined,
open: undefined,
proxy: undefined,
cors: undefined,
headers: undefined
},
envDir: '<作業ディレクトリ>/vite/packages/vite/src/node/__tests__/packages/build-project',
env: { BASE_URL: '/', MODE: 'production', DEV: true, PROD: false },
assetsInclude: [Function: assetsInclude],
logger: {
hasWarned: false,
info: [Function: info],
warn: [Function: warn],
warnOnce: [Function: warnOnce],
error: [Function: error],
clearScreen: [Function: clearScreen],
hasErrorLogged: [Function: hasErrorLogged]
},
packageCache: Map(6) {
'fnpd_<作業ディレクトリ>/vite/packages/vite' => {
dir: '<作業ディレクトリ>/vite/packages/vite',
data: [Object],
hasSideEffects: [Function: hasSideEffects],
webResolvedImports: {},
nodeResolvedImports: {},
setResolvedCache: [Function: setResolvedCache],
getResolvedCache: [Function: getResolvedCache]
},
'fnpd_<作業ディレクトリ>/vite/packages/vite/src/node/__tests__/packages/build-project' => {
dir: '<作業ディレクトリ>/vite/packages/vite',
data: [Object],
hasSideEffects: [Function: hasSideEffects],
webResolvedImports: {},
nodeResolvedImports: {},
setResolvedCache: [Function: setResolvedCache],
getResolvedCache: [Function: getResolvedCache]
},
'fnpd_<作業ディレクトリ>/vite/packages/vite/src/node/__tests__/packages' => {
dir: '<作業ディレクトリ>/vite/packages/vite',
data: [Object],
hasSideEffects: [Function: hasSideEffects],
webResolvedImports: {},
nodeResolvedImports: {},
setResolvedCache: [Function: setResolvedCache],
getResolvedCache: [Function: getResolvedCache]
},
'fnpd_<作業ディレクトリ>/vite/packages/vite/src/node/__tests__' => {
dir: '<作業ディレクトリ>/vite/packages/vite',
data: [Object],
hasSideEffects: [Function: hasSideEffects],
webResolvedImports: {},
nodeResolvedImports: {},
setResolvedCache: [Function: setResolvedCache],
getResolvedCache: [Function: getResolvedCache]
},
'fnpd_<作業ディレクトリ>/vite/packages/vite/src/node' => {
dir: '<作業ディレクトリ>/vite/packages/vite',
data: [Object],
hasSideEffects: [Function: hasSideEffects],
webResolvedImports: {},
nodeResolvedImports: {},
setResolvedCache: [Function: setResolvedCache],
getResolvedCache: [Function: getResolvedCache]
},
'fnpd_<作業ディレクトリ>/vite/packages/vite/src' => {
dir: '<作業ディレクトリ>/vite/packages/vite',
data: [Object],
hasSideEffects: [Function: hasSideEffects],
webResolvedImports: {},
nodeResolvedImports: {},
setResolvedCache: [Function: setResolvedCache],
getResolvedCache: [Function: getResolvedCache]
},
set: [Function (anonymous)]
},
createResolver: [Function: createResolver],
optimizeDeps: { disabled: 'build', esbuildOptions: { preserveSymlinks: false } },
worker: {
format: 'iife',
plugins: [AsyncFunction: createWorkerPlugins],
rollupOptions: {}
},
appType: 'spa',
experimental: { importGlobRestoreExtension: false, hmrPartialAccept: false },
getSortedPlugins: [Function: getSortedPlugins],
getSortedPluginHooks: [Function: getSortedPluginHooks]
}
ビルド時の処理を追うという意味では、デフォルトで使用される各プラグインも見て行った方が良さそうだけど、一旦後回しにする。
各設定を分解する
設定の中から、build・ssr・lib(ライブラリモード)をそれぞれ抽出している。
パスのヘルパー関数を作成
ヘルパー関数側でルートを設定してくれるので、resolve 関数の呼び出し側はその先のパスを指定するだけで良い。便利〜!
複数箇所でパス解決する場面が出てきたらこの方法を参考にしたい。
入力ファイルを決定する
入力ファイルは SSR・ライブラリモード・それ以外で参照するプロパティが異なるっぽい。
特にライブラリモードの場合、エントリーポイントは複数の形式(文字列、配列、オブジェクト)をサポートしているようなので分岐が少し複雑。
入力ファイルのチェック
具体的には、SSR を指定しているかつ、input に.html
のファイルを指定していたり、build.cssCodeSplitを有効にした状態で input に .css
のファイルを指定している場合にエラーにする。
出力先のディレクトリを決定する
シンプルにビルドアセットの出力先を決定してる。
SSR の場合はプラグインにSSR用のフラグを注入する
今回は SSR 周りは深掘りしないので、このフラグがどこで使われるかまでは調査しない。
SSR 周りが気になる場合は、この辺りも調べてみると面白いかもしれない。
依存関係の最適化
依存関係の最適化オプションが有効であれば最適化の処理を実行する。
この辺りの話だと思うけど、長くなると思うので一旦スルー。
build の全体像をざっと確認してから見ようかな。
Rollup に渡すパラメータを整形する
これまで設定してきたプロパティを Rollup に渡せるように整形しています。
テストコードを実行した時の rollupOptions の中身
{
dir: outDir,
// Default format is 'es' for regular and for SSR builds
format,
exports: 'auto',
sourcemap: options.sourcemap,
name: libOptions ? libOptions.name : undefined,
// es2015 enables `generatedCode.symbols`
// - #764 add `Symbol.toStringTag` when build es module into cjs chunk
// - #1048 add `Symbol.toStringTag` for module default export
generatedCode: 'es2015',
entryFileNames: ssr
? `[name].${jsExt}`
: libOptions
? ({ name }) =>
resolveLibFilename(
libOptions,
format,
name,
config.root,
jsExt,
config.packageCache,
)
: path.posix.join(options.assetsDir, `[name]-[hash].${jsExt}`),
chunkFileNames: libOptions
? `[name]-[hash].${jsExt}`
: path.posix.join(options.assetsDir, `[name]-[hash].${jsExt}`),
assetFileNames: libOptions
? `[name].[ext]`
: path.posix.join(options.assetsDir, `[name]-[hash].[ext]`),
inlineDynamicImports:
output.format === 'umd' ||
output.format === 'iife' ||
(ssrWorkerBuild &&
(typeof input === 'string' || Object.keys(input).length === 1)),
...output,
}
以降も主にパラメータ調整っぽいので、ひとまずここまでとしよう