Closed14

Vite 本体のコードを読む(build編)

Keita HinoKeita Hino

モチベーション

仕事やプライベートで Vite を使うことが増えてきた。
なんとなく中で何が行われているのか気になってきたので本体のコードを見てみる。
最初から全てのコードを読むことはできないので、今回はビルドに絞っています。

前提

2023/12/31 時点の main のコードを見ていきます。

コードリーディング後の理解

ざっくりやってること

  • 依存関係の最適化
  • Rollup 用のパラメータを調整 & Rollup でビルドする時に調整したパラメータを渡す
Keita HinoKeita Hino

build 時に実行される処理を見つける

ざっと本体のコードを見ていたところ、いかにも build 時に実行されそうな関数を見つけた。
https://github.com/vitejs/vite/blob/main/packages/vite/src/node/build.ts#L455-L708

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

Keita HinoKeita Hino

設定ファイルを解決する

resolveConfig 関数を使ってユーザーが指定した設定とデフォルトの設定をマージしている。
inlineConfig はユーザーが指定した設定。
https://github.com/vitejs/vite/blob/main/packages/vite/src/node/build.ts#L462-L467

テストコードを実行した時に 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]
}

ビルド時の処理を追うという意味では、デフォルトで使用される各プラグインも見て行った方が良さそうだけど、一旦後回しにする。

Keita HinoKeita Hino

入力ファイルを決定する

入力ファイルは SSR・ライブラリモード・それ以外で参照するプロパティが異なるっぽい。
https://ja.vitejs.dev/config/build-options.html#build-lib

特にライブラリモードの場合、エントリーポイントは複数の形式(文字列、配列、オブジェクト)をサポートしているようなので分岐が少し複雑。
https://github.com/vitejs/vite/blob/main/packages/vite/src/node/build.ts#L481-L495

Keita HinoKeita Hino

Rollup に渡すパラメータを整形する

これまで設定してきたプロパティを Rollup に渡せるように整形しています。
https://github.com/vitejs/vite/blob/main/packages/vite/src/node/build.ts#L528-L542

テストコードを実行した時の 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,
}
Keita HinoKeita Hino

以降も主にパラメータ調整っぽいので、ひとまずここまでとしよう

このスクラップは4ヶ月前にクローズされました