🌊

Vite⚡️のコード散歩 (1)

2023/10/09に公開

概要

内容

ローカルでviteを立ち上げるにはpnpmが必要なので落としてくる

curl -fsSL https://get.pnpm.io/install.sh | sh -

viteのコードを落としてきて

pnpm i
pnpm run build

でbuildまですることができる。簡単

cli.tsを覗く

cliの呼び出し口から簡単に読んでいく

cacと呼ばれるnodeのcliライブラリを利用している。
https://vscode.dev/github/vitejs/vite/blob/main/packages/vite/src/node/cli.ts#L13

cac = Command And Conquerの略っぽい、nodeでcliを作成するのであれば便利なライブラリ
https://github.com/cacjs/cac

vite devの場合のコマンドを読んでいく。
https://vscode.dev/github/vitejs/vite/blob/main/packages/vite/src/node/cli.ts#L134-L232

cliが呼び出されると、serverを立ち上げているように見える。./serverのコードを読みにいく。
createServer関数を引っ張ってきている。

    // output structure is preserved even after bundling so require()
    // is ok here
    const { createServer } = await import('./server')
    try {
      const server = await createServer({
        root,
        base: options.base,
        mode: options.mode,
        configFile: options.config,
        logLevel: options.logLevel,
        clearScreen: options.clearScreen,
        optimizeDeps: { force: options.force },
        server: cleanOptions(options),
      })
    // 省略  
    }

createServer関数はViteDevServer型のobjectを返却する。

export interface ViteDevServer {
 // ...
}

chatgptに型定義の中身を解説してもらう。

この型定義は、Vite開発サーバー(ViteDevServer)のインターフェースを示しています。各プロパティやメソッドがどのような機能を持っているのかを簡単に日本語で説明します。

1. **config: ResolvedConfig**  
   解決されたViteの設定オブジェクトです。

2. **middlewares: Connect.Server**  
   Connectアプリのインスタンスです。カスタムのミドルウェアを開発サーバーに追加するのに使います。

3. **httpServer: http.Server | null**  
   Node.jsのHTTPサーバーインスタンス。ミドルウェアモードでは`null`になります。

4. **watcher: FSWatcher**  
   ファイルシステムの変更を監視するchokidarのインスタンス。

5. **ws: WebSocketServer**  
   WebSocketサーバーのインスタンス。`send(payload)`メソッドを持っています。

6. **pluginContainer: PluginContainer**  
   Rollupプラグインコンテナで、特定のファイルでプラグインのフックを実行できます。

7. **moduleGraph: ModuleGraph**  
   インポート関係、URLとファイルのマッピング、HMR(Hot Module Replacement)の状態を追跡する。

8. **resolvedUrls: ResolvedServerUrls | null**  
   CLIに表示される解決されたURL。`server.listen`が呼ばれる前かミドルウェアモードでは`null`。

9. **transformRequest(...) / transformIndexHtml(...) / ssrTransform(...) etc.**  
   これらは、リクエスト、HTML、SSR(Server Side Rendering)の変換などに関するメソッドです。

10. **listen(), close(), restart()**  
    サーバーの開始、停止、再起動に関するメソッド。

11. **printUrls(), openBrowser()**  
    サーバーのURLを表示したり、ブラウザを開いたりする。

12. **その他(_importGlobMap, _restartPromise, etc.)**  
    内部用のプロパティやメソッド。通常は直接触らない。

_createServer関数内でserver(ViteDevServer型)を組み立てている。

cli.tsに戻ると最初に呼び出されているのはserver.listen()メソッド

/vite/packages/vite/src/node/cli.ts
      await server.listen()

server組み立て時のlisten関数内をみると以下のようになっている。startServer関数が気になる。

packages/vite/src/node/server/index.ts
    async listen(port?: number, isRestart?: boolean) {
      await startServer(server, port)
      if (httpServer) {
        server.resolvedUrls = await resolveServerUrls(
          httpServer,
          config.server,
          config,
        )
        if (!isRestart && config.server.open) server.openBrowser()
      }
      return server
    },

なんだかんだあって、httpServerStart関数が呼び出されている。nodeの標準ライブラリhttp, httpsを利用して実装されている。

packages/vite/src/node/http.ts
export async function httpServerStart(
  httpServer: HttpServer,
  serverOptions: {
    port: number
    strictPort: boolean | undefined
    host: string | undefined
    logger: Logger
  },
): Promise<number> {
  let { port, strictPort, host, logger } = serverOptions

  return new Promise((resolve, reject) => {
    const onError = (e: Error & { code?: string }) => {
      if (e.code === 'EADDRINUSE') {
        if (strictPort) {
          httpServer.removeListener('error', onError)
          reject(new Error(`Port ${port} is already in use`))
        } else {
          logger.info(`Port ${port} is in use, trying another one...`)
          httpServer.listen(++port, host)
        }
      } else {
        httpServer.removeListener('error', onError)
        reject(e)
      }
    }

    httpServer.on('error', onError)

    httpServer.listen(port, host, () => {
      httpServer.removeListener('error', onError)
      resolve(port)
    })
  })
}

code内をみると以下のように記載されている。restartの時ではなくてconfig.server.openがtrueの時はブラウザでリンクを開いてくれるらしい。

 if (!isRestart && config.server.open) server.openBrowser()

cliに戻る。ローカルで立ち上げる時にでてくるVITE v5.0.0-beta.4 ready in 610 msみたいな文字を出力している箇所を見つけた。

      const startupDurationString = viteStartTime
        ? colors.dim(
            `ready in ${colors.reset(
              colors.bold(Math.ceil(performance.now() - viteStartTime)),
            )} ms`,
          )
        : ''

colorsはpicocolorsと呼ばれるライブラリを利用しているらしい。

import colors from 'picocolors'

https://github.com/alexeyraspopov/picocolors

ANSIカラーを用いたターミナル出力のフォーマットに関する、非常に小さく高速なライブラリです。
READMEに記載されている特徴をみると以下のような特徴を持つライブラリだそう。

  • 依存関係なし。
  • chalkよりも14倍小さく、2倍高速。
  • PostCSS、SVGO、Stylelint、Browserslistなどの人気のあるツールで使用されています。
  • Node.js v6以上とブラウザに対応。CJSとESMの両方のプロジェクトに対応。
  • TypeScriptの型宣言が含まれています。
  • NO_COLORフレンドリー。

cliのactionとしてはここで終了。

listen内で呼び出されている以下の関数が重要そう。moduleGraph

const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) =>
	container.resolveId(url, undefined, { ssr }),
)

以下のサイトにmoduleGraphの中身が記載されていた
https://ja.vitejs.dev/guide/api-javascript.html

viteDevServer型の中のpropertyの一つ。

  /**
   * インポートの関係、URL からファイルへのマッピング、HMR の状態を追跡する
   * モジュールグラフ。
   */
  moduleGraph: ModuleGraph

packages/vite/src/node/server/moduleGraph.tsに実装の実態がある。
クラスを読み込んでもらってgptに解説をお願いする。

このTypeScriptのコードでは、`ModuleGraph`という名前のクラスが定義されています。以下は、そのクラス内の主要な関数とそれぞれの機能についての簡単な説明です。

### データメンバ

- `urlToModuleMap`, `idToModuleMap`, `fileToModulesMap`, etc.: これらはマップやセットで、モジュールの情報を管理しています。

### コンストラクタ

- `constructor(resolveId: Function)`: インスタンス生成時にID解決用の関数を受け取ります。

### メソッド

1. **getModuleByUrl**: URLを指定して対応するモジュールを取得します。
   
2. **getModuleById**: IDを指定して対応するモジュールを取得します。
   
3. **getModulesByFile**: ファイル名を指定して対応するモジュールのセットを取得します。
   
4. **onFileChange**: ファイルが変更された場合に、そのファイルに関連するモジュールを無効化します。
   
5. **invalidateModule**: 特定のモジュールを無効化します。
  
6. **invalidateAll**: すべてのモジュールを無効化します。

7. **updateModuleInfo**: モジュールの依存関係情報を更新します。

8. **ensureEntryFromUrl**: 与えられたURLからモジュールエントリーを確保します。

9. **createFileOnlyEntry**: ファイルのみのエントリを作成します。

10. **resolveUrl**: 与えられたURLを解決します。

いくつかのメソッドは内部用(`@internal`)とされており、クラスの内部でのみ使用される想定です(例:`_ensureEntryFromUrl`, `_getUnresolvedUrlToModule`, `_setUnresolvedUrlToModule`, `_resolveUrl`)。

このクラスは、モジュールの依存関係を管理し、動的にモジュールを追加/削除/更新するための一連のメソッドを提供しています。これは、特にHMR(Hot Module Replacement)などのリアルタイムなコード変更を効率的に反映させるために有用です。

Graphの中のNodeクラスは以下のようになっている。

export class ModuleNode {
  /**
   * Public served url path, starts with /
   */
  url: string
  /**
   * Resolved file system path + query
   */
  id: string | null = null
  file: string | null = null
  type: 'js' | 'css'
  info?: ModuleInfo
  meta?: Record<string, any>
  importers = new Set<ModuleNode>()
  clientImportedModules = new Set<ModuleNode>()
  ssrImportedModules = new Set<ModuleNode>()
  acceptedHmrDeps = new Set<ModuleNode>()
  acceptedHmrExports: Set<string> | null = null
  importedBindings: Map<string, Set<string>> | null = null
  isSelfAccepting?: boolean
  transformResult: TransformResult | null = null
  ssrTransformResult: TransformResult | null = null
  ssrModule: Record<string, any> | null = null
  ssrError: Error | null = null
  lastHMRTimestamp = 0
  lastInvalidationTimestamp = 0

  /**
   * @param setIsSelfAccepting - set `false` to set `isSelfAccepting` later. e.g. #7870
   */
  constructor(url: string, setIsSelfAccepting = true) {
    this.url = url
    this.type = isDirectCSSRequest(url) ? 'css' : 'js'
    if (setIsSelfAccepting) {
      this.isSelfAccepting = false
    }
  }

  get importedModules(): Set<ModuleNode> {
    const importedModules = new Set(this.clientImportedModules)
    for (const module of this.ssrImportedModules) {
      importedModules.add(module)
    }
    return importedModules
  }
}

gptさんに解説をお願いする。

この`ModuleNode`クラスは、一見するとJavaScriptやTypeScriptのモジュール管理に関連する多くの情報と機能を持っています。各プロパティとメソッドについて説明します。

### プロパティ

- `url`: モジュールの公開されているURLパス。通常は `/` で始まる。
- `id`: ファイルシステム上での解決されたパス。クエリ文字列も含む可能性があります。
- `file`: ファイルのパス(通常は絶対パス)。
- `type`: モジュールのタイプ。'js'または'css'。
- `info`: モジュールに関する追加情報。`ModuleInfo` 型。
- `meta`: メタデータを保持するオブジェクト。
- `importers`, `clientImportedModules`, `ssrImportedModules`: このモジュールをインポートしている他のモジュール。
- `acceptedHmrDeps`, `acceptedHmrExports`: Hot Module Replacement(HMR)に関連するプロパティ。
- `importedBindings`: このモジュールによってインポートされたバインディング。
- `isSelfAccepting`: このモジュールが自身をHMRで受け入れるかどうか。
- `transformResult`, `ssrTransformResult`: トランスフォーム結果。
- `ssrModule`: サーバーサイドレンダリング(SSR)に関連するモジュールの状態。
- `ssrError`: SSR中に発生したエラー。
- `lastHMRTimestamp`, `lastInvalidationTimestamp`: タイムスタンプ情報。

### メソッド

- `constructor`: インスタンスを初期化する。`url` と `setIsSelfAccepting` を引数に取ります。
- `importedModules`: クライアントとSSRでインポートされたモジュールの集合を返す。

このクラスはモジュールの依存関係、HMRの状態、SSRの状態など、モジュールのライフサイクル全体を管理する役割を持っていると推測されます。

フレームワークやテクニックに依存する解釈もあるため、このクラスがどのようなコンテキストで使われているのか理解すると、より深い理解が得られるでしょう。

ModuleNodeクラスのinfomoduleInfo型なのですが、rollupから持ってきている。

ModuleGraphは名前の通り、モジュール間の依存関係を追跡するものっぽい。

他に雑に読んだところによると、websocketにwsと呼ばれるライブラリを利用している。
https://www.npmjs.com/package/ws

ws.sendでHMR(Hot Module Replacement)の更新を飛ばしている。

  config.logger.info(
    colors.green(`hmr update `) +
      colors.dim([...new Set(updates.map((u) => u.path))].join(', ')),
    { clear: !afterInvalidation, timestamp: true },
  )
  ws.send({
    type: 'update',
    updates,
  })

ModuleGraphが更新された際にupdateを呼び出している。

以下でlistenされている。

  // Listen for messages
  socket.addEventListener('message', async ({ data }) => {
    handleMessage(JSON.parse(data))
  })

handleMessage内でupdateを検知して何かをしていそう。

packages/vite/src/client/client.ts
    case 'update':
      notifyListeners('vite:beforeUpdate', payload)
      // if this is the first update and there's already an error overlay, it
      // means the page opened with existing server compile error and the whole
      // module script failed to load (since one of the nested imports is 500).
      // in this case a normal update won't work and a full reload is needed.
      if (isFirstUpdate && hasErrorOverlay()) {
        window.location.reload()
        return
      } else {
        clearErrorOverlay()
        isFirstUpdate = false
      }
      await Promise.all(
        payload.updates.map(async (update): Promise<void> => {
          if (update.type === 'js-update') {
            return queueUpdate(fetchUpdate(update))
          }

          // css-update
          // this is only sent when a css file referenced with <link> is updated
          const { path, timestamp } = update
          const searchUrl = cleanUrl(path)
          // can't use querySelector with `[href*=]` here since the link may be
          // using relative paths so we need to use link.href to grab the full
          // URL for the include check.
          const el = Array.from(
            document.querySelectorAll<HTMLLinkElement>('link'),
          ).find(
            (e) =>
              !outdatedLinkTags.has(e) && cleanUrl(e.href).includes(searchUrl),
          )

          if (!el) {
            return
          }

          const newPath = `${base}${searchUrl.slice(1)}${
            searchUrl.includes('?') ? '&' : '?'
          }t=${timestamp}`

          // rather than swapping the href on the existing tag, we will
          // create a new link tag. Once the new stylesheet has loaded we
          // will remove the existing link tag. This removes a Flash Of
          // Unstyled Content that can occur when swapping out the tag href
          // directly, as the new stylesheet has not yet been loaded.
          return new Promise((resolve) => {
            const newLinkTag = el.cloneNode() as HTMLLinkElement
            newLinkTag.href = new URL(newPath, el.href).href
            const removeOldEl = () => {
              el.remove()
              console.debug(`[vite] css hot updated: ${searchUrl}`)
              resolve()
            }
            newLinkTag.addEventListener('load', removeOldEl)
            newLinkTag.addEventListener('error', removeOldEl)
            outdatedLinkTags.add(el)
            el.after(newLinkTag)
          })
        }),
      )
      notifyListeners('vite:afterUpdate', payload)
      break

一旦今日はここまで

Discussion