Viteのコードを読む - CLI経由でのコア実行の流れ

前回: Viteのコードを読む - リポジトリ直下のファイル群 - memo_md

コアのファイルをよむ

  • packages/vite を見ていく
  • とりあえずディレクトリ構造を眺めてみる

ディレクトリ構造

.
├── bin
├── dist
│   ├── client
│   └── node
│       └── chunks
├── scripts
├── src
│   ├── client
│   └── node
│       ├── __tests__
│       │   ├── packages
│       │   │   ├── name
│       │   │   └── noname
│       │   └── plugins
│       ├── optimizer
│       ├── plugins
│       ├── server
│       │   ├── __tests__
│       │   │   └── fixtures
│       │   │       ├── lerna
│       │   │       │   └── nested
│       │   │       ├── none
│       │   │       │   └── nested
│       │   │       ├── pnpm
│       │   │       │   └── nested
│       │   │       └── yarn
│       │   │           └── nested
│       │   └── middlewares
│       └── ssr
│           └── __tests__
└── types
  • 実はそんなにディレクトリはないっぽい
  • 普段はCLI経由で叩くことが多いので、CLIからどういった流れでコアのコードが実行されているのかを追ってみる

CLI経由でのコマンド実行の流れ

  1. bin/vite.js
  2. dist/node/cli
  3. コマンドに応じた振り分け

bin/vite.js

  • source-map-supportのインストール (StackTraceから追えるようにぽい)
  • --profile オプションに応じた挙動の切り替え
    • APIドキュメントとかには記述がない
    • new inspector.Session() を呼び出しているのでデバッグ用?
  • require('../dist/node/cli') … 実行本体
    • 実際には packages/vite/src/node/cli.ts

dist/node/cli (packages/vite/src/node/cli.ts)

コマンドに応じた振り分け

[root]

  • vite起動本体。ViteDevServerが立ち上がる(実体は https://nodejs.org/api/http.html
  • packages/vite/src/node/server/index.ts # createServer
  • CLIオプションを inlineConfig として渡す

build

  • 本番用build
  • packages/vite/src/node/build.ts # build

optimize

  • packages/vite/src/node/optimizer.ts # optimizeDeps
  • https://vitejs.dev/guide/dep-pre-bundling.html
  • 通称 dependency pre-bundling の実行
  • 一部依存を事前解決しておくことで、vite serve 時のパフォーマンス向上に繋がるぽい

preview


  • 依存しているライブラリ (cac とか picocolors とか) は自分でも使えそう
  • CLI経由の場合オプションはそれほどない。まだ読めるって感じする
  • コア(root)の中に入るとそれなりに大きい。そこからが本番って感じ

次 → 振り分けている各コマンドの詳細を見ていく

Viteのコードを読む - リポジトリ直下のファイル群

前回: Viteのコードを読む - ディレクトリ構造とテスト実行 - memo_md


リポジトリ直下のファイル群をみてみる

チェックアウトしたリポジトリの直下にあるファイル群。 主に設定ファイルとかそういう系を眺めてみる。

一覧

  • package.json
  • LICENSE
  • CODE_OF_CONDUCT.md
  • CONTRIBUTING.md
  • jest.config.ts
  • netlify.toml
  • pnpm-lock.yaml
  • pnpm-workspace.yaml
  • README.md

LICENCEとかそういうのは省略。必要そうなのを見ていく。

package.json

  • node ≥ 12.2.0
  • scripts
    • preinstallnpx only-allow pnpm
      • https://github.com/pnpm/only-allow
      • 利用可能なパッケージマネージャを強制するツールらしい。こんなのあったのか..
      • 試しに yarn install とかすると Use "pnpm install" for installation in this project. とエラーにしてくれる
    • formatlint はよくある感じ。eslintやprettierを実行してる。
    • 他は jest実行系, vitepressによるドキュメント生成系、viteやpluginのビルド系、などがある
  • lint-staged
    • https://github.com/okonet/lint-staged
    • gitでstageされたファイルを対象にlint,prettierを実行
    • prettierは全ファイル。eslintはtsファイルのみ対象に実行している
  • simple-git-hooks

pnpm-workspace.yaml / pnpm-lock.yaml

以下をmonorepos対象にしてる

  • packages/*
  • packages/playground/**

jest.config.ts

  • ts-jest利用
    • 意外とテスト実行時に型チェックしてるんだなという感想
  • VITE_TEST_BUILD 指定時は実行対象を playground 配下に絞ってる
    • npm script の test-build test-serve の呼び分けで使う
    • 開発サーバモードでテストするか、ビルドモードでテストするかの違い
    • CONTRIBUTING.md にそのあたりの説明がある
  • globalSetup: './scripts/jestGlobalSetup.cjs'
    • playwright-chromium で browser を起動
    • packages/temp に playground のファイルをコピーしてる?
  • globalTeardown: './scripts/jestGlobalTeardown.cjs'
    • global.__BROWSER_SERVER__ にサーバーがあれば閉じる
    • packages/temp を消して後片付け
  • testEnvironment: './scripts/jestEnv.cjs'
    • 独自のEnvironmentを実装して適用
    • setup
      • wsEndpoint を参照して、ブラウザで接続
      • global.page に新しく開いたブラウザのページを保持
    • teerdown
      • ブラウザを閉じてる
  • setupFilesAfterEnv: ['./scripts/jestPerTestSetup.ts']
    • 各 Playground のテストディレクトリにある serve.js があれば実行する
      • ビルドやサーバ起動を行ってる
      • requireしてみて serve or preServe があれば実行という感じ
    • isBuildTest (VITE_TEST_BUILD で決まる)の値に応じて page から繋ぐ接続先のURLを切り替えてる
      • isBuildTest がtruthyならビルドされる
      • Viteのサーバではなく、ビルド結果を使って http パッケージによる配信を行う

CODE_OF_CONDUCT.md

健全なOSS運営についてのポリシーが書いてある。

大事。

CONTRIBUTING.md

  • リポジトリのセットアップ方法
    • pnpm 周りの導入あたりがわかる
  • テスト実行周り
    • playwright使ってますよ〜という話
    • 開発サーバモードとビルドモードの違い
    • テストの書き方
  • デバッグログの出力方法
  • PRのガイドライン
  • Dependencies周りの注意点
    • 小さく保つためRollupでバンドルするのが前提で、基本的にdevDependenciesになる
    • requireは使えないので dynamic import を使う必要がある
    • 依存関係が大きく、機能のわりにサイズが大きいものは使わない
    • 結構厳しい感じ。自力で作れるものは作りましょうって感じかもしれん
  • 型付けを完璧に維持してますという話
  • オプション追加、ホントに必要かよく考えろよという話
    • 価値があるのか、他に方法(別オプションやプラグインでの解決)が無いのか、など
  • 翻訳や Vite Land (https://chat.vitejs.dev) の話

感想

  • JestのEnvironment独自で作ってるのはナルホドって感じだった
  • 依存関係気軽に足せないのは大変そう
  • lint-staged とか普通に知らんかった..、便利そうなので使っていきたい

次→Viteのコアコードを少しずつ読んでく

Viteのコードを読む - ディレクトリ構造とテスト実行

(全然ブログ書いてなかった...)

Viteのコードを読んでみようという試み

Viteが盛り上がっていて個人的にも使っているので中身を少しでも把握したい。

大変だろうけど、バージョンが上がりまくって機能追加されてからだと中身追うのも無理になりそうなので、今のうちに見れるだけ見てみるという試み。心が折れたらやめるかもしれない。

読む対象

  • v2.8.3
  • hash: e2349569cf96e506e0d5fff1d043727a77fdad70

ファイル一覧を眺める

とりあえず何も考えずに tree コマンドで列挙してみた

見てみる(クリックで展開)

.
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── docs
│   ├── blog
│   │   └── announcing-vite2.md
│   ├── config
│   │   └── index.md
│   ├── guide
│   │   ├── api-hmr.md
│   │   ├── api-javascript.md
│   │   ├── api-plugin.md
│   │   ├── assets.md
│   │   ├── backend-integration.md
│   │   ├── build.md
│   │   ├── comparisons.md
│   │   ├── dep-pre-bundling.md
│   │   ├── env-and-mode.md
│   │   ├── features.md
│   │   ├── index.md
│   │   ├── migration.md
│   │   ├── ssr.md
│   │   ├── static-deploy.md
│   │   ├── using-plugins.md
│   │   └── why.md
│   ├── images
│   │   ├── bundler.png
│   │   ├── esm.png
│   │   ├── graph.png
│   │   ├── vercel-configuration.png
│   │   └── vite-plugin-inspect.png
│   ├── index.md
│   ├── plugins
│   │   └── index.md
│   └── public
│       ├── _headers
│       ├── cypress.svg
│       ├── divriots.png
│       ├── logo.svg
│       ├── mux.svg
│       ├── plaid.svg
│       ├── stackblitz.svg
│       ├── tailwind-labs.svg
│       ├── vite.mp3
│       ├── voice.svg
│       └── vuejobs.png
├── jest.config.ts
├── netlify.toml
├── package.json
├── packages
│   ├── create-vite
│   │   ├── CHANGELOG.md
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── __tests__
│   │   │   └── cli.spec.ts
│   │   ├── index.js
│   │   ├── package.json
│   │   ├── template-lit
│   │   │   ├── _gitignore
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── src
│   │   │   │   ├── favicon.svg
│   │   │   │   └── my-element.js
│   │   │   └── vite.config.js
│   │   ├── template-lit-ts
│   │   │   ├── _gitignore
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── src
│   │   │   │   ├── favicon.svg
│   │   │   │   ├── my-element.ts
│   │   │   │   └── vite-env.d.ts
│   │   │   ├── tsconfig.json
│   │   │   ├── tsconfig.node.json
│   │   │   └── vite.config.ts
│   │   ├── template-preact
│   │   │   ├── _gitignore
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── src
│   │   │   │   ├── app.jsx
│   │   │   │   ├── favicon.svg
│   │   │   │   ├── index.css
│   │   │   │   ├── logo.jsx
│   │   │   │   └── main.jsx
│   │   │   └── vite.config.js
│   │   ├── template-preact-ts
│   │   │   ├── _gitignore
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── src
│   │   │   │   ├── app.tsx
│   │   │   │   ├── favicon.svg
│   │   │   │   ├── index.css
│   │   │   │   ├── logo.tsx
│   │   │   │   ├── main.tsx
│   │   │   │   ├── preact.d.ts
│   │   │   │   └── vite-env.d.ts
│   │   │   ├── tsconfig.json
│   │   │   ├── tsconfig.node.json
│   │   │   └── vite.config.ts
│   │   ├── template-react
│   │   │   ├── _gitignore
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── src
│   │   │   │   ├── App.css
│   │   │   │   ├── App.jsx
│   │   │   │   ├── favicon.svg
│   │   │   │   ├── index.css
│   │   │   │   ├── logo.svg
│   │   │   │   └── main.jsx
│   │   │   └── vite.config.js
│   │   ├── template-react-ts
│   │   │   ├── _gitignore
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── src
│   │   │   │   ├── App.css
│   │   │   │   ├── App.tsx
│   │   │   │   ├── favicon.svg
│   │   │   │   ├── index.css
│   │   │   │   ├── logo.svg
│   │   │   │   ├── main.tsx
│   │   │   │   └── vite-env.d.ts
│   │   │   ├── tsconfig.json
│   │   │   ├── tsconfig.node.json
│   │   │   └── vite.config.ts
│   │   ├── template-svelte
│   │   │   ├── README.md
│   │   │   ├── _gitignore
│   │   │   ├── index.html
│   │   │   ├── jsconfig.json
│   │   │   ├── package.json
│   │   │   ├── public
│   │   │   │   └── favicon.ico
│   │   │   ├── src
│   │   │   │   ├── App.svelte
│   │   │   │   ├── assets
│   │   │   │   │   └── svelte.png
│   │   │   │   ├── lib
│   │   │   │   │   └── Counter.svelte
│   │   │   │   ├── main.js
│   │   │   │   └── vite-env.d.ts
│   │   │   └── vite.config.js
│   │   ├── template-svelte-ts
│   │   │   ├── README.md
│   │   │   ├── _gitignore
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── public
│   │   │   │   └── favicon.ico
│   │   │   ├── src
│   │   │   │   ├── App.svelte
│   │   │   │   ├── assets
│   │   │   │   │   └── svelte.png
│   │   │   │   ├── lib
│   │   │   │   │   └── Counter.svelte
│   │   │   │   ├── main.ts
│   │   │   │   └── vite-env.d.ts
│   │   │   ├── svelte.config.js
│   │   │   ├── tsconfig.json
│   │   │   ├── tsconfig.node.json
│   │   │   └── vite.config.ts
│   │   ├── template-vanilla
│   │   │   ├── _gitignore
│   │   │   ├── favicon.svg
│   │   │   ├── index.html
│   │   │   ├── main.js
│   │   │   ├── package.json
│   │   │   └── style.css
│   │   ├── template-vanilla-ts
│   │   │   ├── _gitignore
│   │   │   ├── favicon.svg
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── src
│   │   │   │   ├── main.ts
│   │   │   │   ├── style.css
│   │   │   │   └── vite-env.d.ts
│   │   │   └── tsconfig.json
│   │   ├── template-vue
│   │   │   ├── README.md
│   │   │   ├── _gitignore
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── public
│   │   │   │   └── favicon.ico
│   │   │   ├── src
│   │   │   │   ├── App.vue
│   │   │   │   ├── assets
│   │   │   │   │   └── logo.png
│   │   │   │   ├── components
│   │   │   │   │   └── HelloWorld.vue
│   │   │   │   └── main.js
│   │   │   └── vite.config.js
│   │   └── template-vue-ts
│   │       ├── README.md
│   │       ├── _gitignore
│   │       ├── index.html
│   │       ├── package.json
│   │       ├── public
│   │       │   └── favicon.ico
│   │       ├── src
│   │       │   ├── App.vue
│   │       │   ├── assets
│   │       │   │   └── logo.png
│   │       │   ├── components
│   │       │   │   └── HelloWorld.vue
│   │       │   ├── env.d.ts
│   │       │   └── main.ts
│   │       ├── tsconfig.json
│   │       ├── tsconfig.node.json
│   │       └── vite.config.ts
│   ├── playground
│   │   ├── alias
│   │   │   ├── __tests__
│   │   │   │   └── alias.spec.ts
│   │   │   ├── customResolver.js
│   │   │   ├── dir
│   │   │   │   ├── from-script-src.js
│   │   │   │   ├── module
│   │   │   │   │   ├── index.js
│   │   │   │   │   └── package.json
│   │   │   │   ├── test.css
│   │   │   │   └── test.js
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── test.js
│   │   │   └── vite.config.js
│   │   ├── assets
│   │   │   ├── __tests__
│   │   │   │   └── assets.spec.ts
│   │   │   ├── css
│   │   │   │   ├── css-url.css
│   │   │   │   ├── fonts.css
│   │   │   │   ├── icons.css
│   │   │   │   ├── import.css
│   │   │   │   └── nested
│   │   │   │       └── at-imported-css-url.css
│   │   │   ├── fonts
│   │   │   │   ├── Inter-Italic.woff
│   │   │   │   └── Inter-Italic.woff2
│   │   │   ├── foo.js
│   │   │   ├── index.html
│   │   │   ├── nested
│   │   │   │   ├── asset.png
│   │   │   │   ├── fragment-bg.svg
│   │   │   │   ├── fragment.svg
│   │   │   │   ├── icon.png
│   │   │   │   └── �\203\206�\202��\203\210-測試-white\ space.png
│   │   │   ├── package.json
│   │   │   ├── static
│   │   │   │   ├── icon.png
│   │   │   │   ├── import-expression.js
│   │   │   │   ├── raw.css
│   │   │   │   └── raw.js
│   │   │   ├── vite.config.js
│   │   │   └── �\203\206�\202��\203\210-測試-white\ space.js
│   │   ├── backend-integration
│   │   │   ├── __tests__
│   │   │   │   └── backend-integration.spec.ts
│   │   │   ├── frontend
│   │   │   │   ├── entrypoints
│   │   │   │   │   ├── global.css
│   │   │   │   │   ├── index.html
│   │   │   │   │   └── main.ts
│   │   │   │   ├── images
│   │   │   │   │   └── logo.png
│   │   │   │   └── styles
│   │   │   │       ├── background.css
│   │   │   │       └── tailwind.css
│   │   │   ├── package.json
│   │   │   ├── postcss.config.js
│   │   │   ├── references.css
│   │   │   ├── tailwind.config.js
│   │   │   └── vite.config.js
│   │   ├── cli
│   │   │   ├── __tests__
│   │   │   │   ├── cli.spec.ts
│   │   │   │   └── serve.js
│   │   │   ├── index.html
│   │   │   ├── index.js
│   │   │   ├── package.json
│   │   │   └── vite.config.js
│   │   ├── cli-module
│   │   │   ├── __tests__
│   │   │   │   ├── cli-module.spec.ts
│   │   │   │   └── serve.js
│   │   │   ├── index.html
│   │   │   ├── index.js
│   │   │   ├── package.json
│   │   │   └── vite.config.js
│   │   ├── css
│   │   │   ├── __tests__
│   │   │   │   └── css.spec.ts
│   │   │   ├── async-treeshaken.css
│   │   │   ├── async-treeshaken.js
│   │   │   ├── async.css
│   │   │   ├── async.js
│   │   │   ├── composed.module.css
│   │   │   ├── composed.module.less
│   │   │   ├── composed.module.scss
│   │   │   ├── composes-path-resolving.module.css
│   │   │   ├── css-dep
│   │   │   │   ├── index.css
│   │   │   │   ├── index.js
│   │   │   │   ├── index.scss
│   │   │   │   ├── index.styl
│   │   │   │   └── package.json
│   │   │   ├── dep.css
│   │   │   ├── glob-dep
│   │   │   │   ├── bar.css
│   │   │   │   └── foo.css
│   │   │   ├── glob-dep.css
│   │   │   ├── imported-at-import.css
│   │   │   ├── imported.css
│   │   │   ├── index.html
│   │   │   ├── inlined.css
│   │   │   ├── less.less
│   │   │   ├── linked-at-import.css
│   │   │   ├── linked.css
│   │   │   ├── main.js
│   │   │   ├── minify.css
│   │   │   ├── mod.module.css
│   │   │   ├── mod.module.scss
│   │   │   ├── nested
│   │   │   │   ├── _index.scss
│   │   │   │   ├── _partial.scss
│   │   │   │   ├── icon.png
│   │   │   │   ├── nested.less
│   │   │   │   └── nested.styl
│   │   │   ├── ok.png
│   │   │   ├── options
│   │   │   │   ├── absolute-import.styl
│   │   │   │   └── relative-import.styl
│   │   │   ├── package.json
│   │   │   ├── pkg-dep
│   │   │   │   ├── _index.scss
│   │   │   │   ├── index.js
│   │   │   │   └── package.json
│   │   │   ├── postcss-caching
│   │   │   │   ├── blue-app
│   │   │   │   │   ├── imported.css
│   │   │   │   │   ├── index.html
│   │   │   │   │   ├── main.js
│   │   │   │   │   ├── package.json
│   │   │   │   │   └── postcss.config.js
│   │   │   │   ├── css.spec.ts
│   │   │   │   └── green-app
│   │   │   │       ├── imported.css
│   │   │   │       ├── index.html
│   │   │   │       ├── main.js
│   │   │   │       ├── package.json
│   │   │   │       └── postcss.config.js
│   │   │   ├── postcss.config.js
│   │   │   ├── sass.scss
│   │   │   ├── stylus.styl
│   │   │   └── vite.config.js
│   │   ├── css-codesplit
│   │   │   ├── __tests__
│   │   │   │   └── css-codesplit.spec.ts
│   │   │   ├── index.html
│   │   │   ├── main.css
│   │   │   ├── main.js
│   │   │   ├── other.js
│   │   │   ├── package.json
│   │   │   ├── style.css
│   │   │   └── vite.config.js
│   │   ├── css-codesplit-cjs
│   │   │   ├── __tests__
│   │   │   │   └── css-codesplit.spec.ts
│   │   │   ├── index.html
│   │   │   ├── main.css
│   │   │   ├── main.js
│   │   │   ├── other.js
│   │   │   ├── package.json
│   │   │   ├── style.css
│   │   │   └── vite.config.js
│   │   ├── data-uri
│   │   │   ├── __tests__
│   │   │   │   └── data-uri.spec.ts
│   │   │   ├── index.html
│   │   │   └── package.json
│   │   ├── define
│   │   │   ├── __tests__
│   │   │   │   └── define.spec.ts
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   └── vite.config.js
│   │   ├── dynamic-import
│   │   │   ├── __tests__
│   │   │   │   └── dynamic-import.spec.ts
│   │   │   ├── css
│   │   │   │   └── index.css
│   │   │   ├── index.html
│   │   │   ├── mxd.js
│   │   │   ├── mxd.json
│   │   │   ├── nested
│   │   │   │   ├── index.js
│   │   │   │   └── shared.js
│   │   │   ├── package.json
│   │   │   ├── qux.js
│   │   │   ├── views
│   │   │   │   ├── bar.js
│   │   │   │   ├── baz.js
│   │   │   │   └── foo.js
│   │   │   └── vite.config.js
│   │   ├── env
│   │   │   ├── __tests__
│   │   │   │   └── env.spec.ts
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   └── vite.config.js
│   │   ├── extensions
│   │   │   ├── __tests__
│   │   │   │   └── extensions.spec.ts
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   └── vite.config.js
│   │   ├── file-delete-restore
│   │   │   ├── App.jsx
│   │   │   ├── Child.jsx
│   │   │   ├── __tests__
│   │   │   │   └── file-delete-restore.spec.ts
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   └── vite.config.js
│   │   ├── fs-serve
│   │   │   ├── __tests__
│   │   │   │   └── fs-serve.spec.ts
│   │   │   ├── entry.js
│   │   │   ├── nested
│   │   │   │   └── foo.js
│   │   │   ├── package.json
│   │   │   ├── root
│   │   │   │   ├── src
│   │   │   │   │   ├── index.html
│   │   │   │   │   └── safe.txt
│   │   │   │   ├── unsafe.txt
│   │   │   │   └── vite.config.js
│   │   │   ├── safe.json
│   │   │   └── unsafe.json
│   │   ├── glob-import
│   │   │   ├── __tests__
│   │   │   │   └── glob-import.spec.ts
│   │   │   ├── dir
│   │   │   │   ├── baz.json
│   │   │   │   ├── foo.js
│   │   │   │   ├── index.js
│   │   │   │   ├── nested
│   │   │   │   │   └── bar.js
│   │   │   │   └── node_modules
│   │   │   │       └── hoge.js
│   │   │   ├── index.html
│   │   │   └── package.json
│   │   ├── hmr
│   │   │   ├── __tests__
│   │   │   │   └── hmr.spec.ts
│   │   │   ├── customFile.js
│   │   │   ├── global.css
│   │   │   ├── hmr.js
│   │   │   ├── hmrDep.js
│   │   │   ├── hmrNestedDep.js
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── unicode-path
│   │   │   │   └── 中�\226\207-�\201��\201��\202\223�\201\224-�\225\234�\200-�\237\214\225�\237\214\226�\237\214\227
│   │   │   │       └── index.html
│   │   │   └── vite.config.js
│   │   ├── html
│   │   │   ├── __tests__
│   │   │   │   └── html.spec.ts
│   │   │   ├── common.css
│   │   │   ├── emptyAttr.html
│   │   │   ├── foo.html
│   │   │   ├── index.html
│   │   │   ├── inline
│   │   │   │   ├── common.js
│   │   │   │   ├── dep1.js
│   │   │   │   ├── dep2.js
│   │   │   │   ├── dep3.js
│   │   │   │   ├── module-graph.dot
│   │   │   │   ├── shared-1.html
│   │   │   │   ├── shared-2.html
│   │   │   │   ├── shared.js
│   │   │   │   ├── unique.html
│   │   │   │   └── unique.js
│   │   │   ├── invalid.html
│   │   │   ├── link.html
│   │   │   ├── main.css
│   │   │   ├── main.js
│   │   │   ├── nested
│   │   │   │   ├── index.html
│   │   │   │   ├── nested.css
│   │   │   │   └── nested.js
│   │   │   ├── noBody.html
│   │   │   ├── noHead.html
│   │   │   ├── package.json
│   │   │   ├── scriptAsync.html
│   │   │   ├── scriptMixed.html
│   │   │   ├── shared.js
│   │   │   ├── unicode-path
│   │   │   │   └── 中�\226\207-�\201��\201��\202\223�\201\224-�\225\234�\200-�\237\214\225�\237\214\226�\237\214\227
│   │   │   │       └── index.html
│   │   │   ├── vite.config.js
│   │   │   └── zeroJS.html
│   │   ├── json
│   │   │   ├── __tests__
│   │   │   │   └── json.spec.ts
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── public
│   │   │   │   └── public.json
│   │   │   └── test.json
│   │   ├── legacy
│   │   │   ├── __tests__
│   │   │   │   ├── legacy.spec.ts
│   │   │   │   └── ssr
│   │   │   │       ├── legacy-ssr.spec.ts
│   │   │   │       └── serve.js
│   │   │   ├── async.js
│   │   │   ├── entry-server.js
│   │   │   ├── immutable-chunk.js
│   │   │   ├── index.html
│   │   │   ├── main.js
│   │   │   ├── package.json
│   │   │   ├── style.css
│   │   │   ├── vite.config-custom-filename.js
│   │   │   └── vite.config.js
│   │   ├── lib
│   │   │   ├── __tests__
│   │   │   │   ├── lib.spec.ts
│   │   │   │   └── serve.js
│   │   │   ├── index.dist.html
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── src
│   │   │   │   ├── main.js
│   │   │   │   ├── main2.js
│   │   │   │   └── message.js
│   │   │   ├── vite.config.js
│   │   │   └── vite.dyimport.config.js
│   │   ├── multiple-entrypoints
│   │   │   ├── __tests__
│   │   │   │   └── multiple-entrypoints.spec.ts
│   │   │   ├── deps.json
│   │   │   ├── dynamic-a.js
│   │   │   ├── dynamic-b.js
│   │   │   ├── entrypoints
│   │   │   │   ├── a0.js
│   │   │   │   ├── a1.js
│   │   │   │   ├── a10.js
│   │   │   │   ├── a11.js
│   │   │   │   ├── a12.js
│   │   │   │   ├── a13.js
│   │   │   │   ├── a14.js
│   │   │   │   ├── a15.js
│   │   │   │   ├── a16.js
│   │   │   │   ├── a17.js
│   │   │   │   ├── a18.js
│   │   │   │   ├── a19.js
│   │   │   │   ├── a2.js
│   │   │   │   ├── a20.js
│   │   │   │   ├── a21.js
│   │   │   │   ├── a22.js
│   │   │   │   ├── a23.js
│   │   │   │   ├── a24.js
│   │   │   │   ├── a3.js
│   │   │   │   ├── a4.js
│   │   │   │   ├── a5.js
│   │   │   │   ├── a6.js
│   │   │   │   ├── a7.js
│   │   │   │   ├── a8.js
│   │   │   │   └── a9.js
│   │   │   ├── index.html
│   │   │   ├── index.js
│   │   │   ├── package.json
│   │   │   ├── reference.js
│   │   │   ├── reference.scss
│   │   │   └── vite.config.js
│   │   ├── nested-deps
│   │   │   ├── __tests__
│   │   │   │   └── nested-deps.spec.ts
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── test-package-a
│   │   │   │   ├── index.js
│   │   │   │   └── package.json
│   │   │   ├── test-package-b
│   │   │   │   ├── index.js
│   │   │   │   ├── node_modules
│   │   │   │   │   └── test-package-a
│   │   │   │   │       ├── index.js
│   │   │   │   │       └── package.json
│   │   │   │   └── package.json
│   │   │   ├── test-package-c
│   │   │   │   ├── index-es.js
│   │   │   │   ├── index.js
│   │   │   │   ├── package.json
│   │   │   │   └── side.js
│   │   │   ├── test-package-d
│   │   │   │   ├── index.js
│   │   │   │   ├── package.json
│   │   │   │   └── test-package-d-nested
│   │   │   │       ├── index.js
│   │   │   │       └── package.json
│   │   │   ├── test-package-e
│   │   │   │   ├── index.js
│   │   │   │   ├── package.json
│   │   │   │   ├── test-package-e-excluded
│   │   │   │   │   ├── index.js
│   │   │   │   │   └── package.json
│   │   │   │   └── test-package-e-included
│   │   │   │       ├── index.js
│   │   │   │       └── package.json
│   │   │   └── vite.config.js
│   │   ├── optimize-deps
│   │   │   ├── __tests__
│   │   │   │   └── optimize-deps.spec.ts
│   │   │   ├── cjs-dynamic.js
│   │   │   ├── cjs.js
│   │   │   ├── dedupe.js
│   │   │   ├── dep-cjs-compiled-from-cjs
│   │   │   │   ├── index.js
│   │   │   │   └── package.json
│   │   │   ├── dep-cjs-compiled-from-esm
│   │   │   │   ├── index.js
│   │   │   │   └── package.json
│   │   │   ├── dep-esbuild-plugin-transform
│   │   │   │   ├── index.js
│   │   │   │   └── package.json
│   │   │   ├── dep-linked
│   │   │   │   ├── index.js
│   │   │   │   └── package.json
│   │   │   ├── dep-linked-include
│   │   │   │   ├── Test.vue
│   │   │   │   ├── foo.js
│   │   │   │   ├── index.mjs
│   │   │   │   ├── package.json
│   │   │   │   └── test.css
│   │   │   ├── glob
│   │   │   │   └── foo.js
│   │   │   ├── index.html
│   │   │   ├── nested-exclude
│   │   │   │   ├── index.js
│   │   │   │   ├── nested-include
│   │   │   │   │   ├── index.js
│   │   │   │   │   └── package.json
│   │   │   │   └── package.json
│   │   │   ├── package.json
│   │   │   └── vite.config.js
│   │   ├── optimize-missing-deps
│   │   │   ├── __test__
│   │   │   │   ├── optimize-missing-deps.spec.ts
│   │   │   │   └── serve.js
│   │   │   ├── index.html
│   │   │   ├── main.js
│   │   │   ├── missing-dep
│   │   │   │   ├── index.js
│   │   │   │   └── package.json
│   │   │   ├── multi-entry-dep
│   │   │   │   ├── index.browser.js
│   │   │   │   ├── index.js
│   │   │   │   └── package.json
│   │   │   ├── package.json
│   │   │   └── server.js
│   │   ├── package.json
│   │   ├── preload
│   │   │   ├── __tests__
│   │   │   │   └── preload.spec.ts
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── router.js
│   │   │   ├── src
│   │   │   │   ├── App.vue
│   │   │   │   └── components
│   │   │   │       ├── About.vue
│   │   │   │       ├── Hello.vue
│   │   │   │       └── Home.vue
│   │   │   └── vite.config.js
│   │   ├── preserve-symlinks
│   │   │   ├── __tests__
│   │   │   │   └── preserve-symlinks.spec.ts
│   │   │   ├── index.html
│   │   │   ├── moduleA
│   │   │   │   ├── linked.js -> ./src/index.js
│   │   │   │   ├── package.json
│   │   │   │   └── src
│   │   │   │       ├── data.js
│   │   │   │       └── index.js
│   │   │   ├── package.json
│   │   │   └── src
│   │   │       └── main.js
│   │   ├── react
│   │   │   ├── App.jsx
│   │   │   ├── __tests__
│   │   │   │   └── react.spec.ts
│   │   │   ├── components
│   │   │   │   └── Dummy.jsx
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   └── vite.config.ts
│   │   ├── react-emotion
│   │   │   ├── App.jsx
│   │   │   ├── __tests__
│   │   │   │   └── react.spec.ts
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   └── vite.config.ts
│   │   ├── resolve
│   │   │   ├── __tests__
│   │   │   │   └── resolve.spec.ts
│   │   │   ├── browser-field
│   │   │   │   ├── multiple.dot.path.js
│   │   │   │   ├── no-ext-index
│   │   │   │   │   └── index.js
│   │   │   │   ├── no-ext.js
│   │   │   │   ├── not-browser.js
│   │   │   │   ├── out
│   │   │   │   │   ├── cjs.node.js
│   │   │   │   │   └── esm.browser.js
│   │   │   │   ├── package.json
│   │   │   │   └── relative.js
│   │   │   ├── config-dep.js
│   │   │   ├── custom-condition
│   │   │   │   ├── index.custom.js
│   │   │   │   ├── index.js
│   │   │   │   └── package.json
│   │   │   ├── custom-ext.es
│   │   │   ├── custom-main-field
│   │   │   │   ├── index.custom.js
│   │   │   │   ├── index.js
│   │   │   │   └── package.json
│   │   │   ├── dir
│   │   │   │   └── index.js
│   │   │   ├── dir-with-ext
│   │   │   │   └── index.js
│   │   │   ├── dir-with-ext.js
│   │   │   │   └── empty
│   │   │   ├── dir.js
│   │   │   ├── exact-extension
│   │   │   │   ├── file.js
│   │   │   │   └── file.js.js
│   │   │   ├── exports-env
│   │   │   │   ├── browser.js
│   │   │   │   ├── browser.mjs
│   │   │   │   ├── browser.prod.mjs
│   │   │   │   ├── fallback.umd.js
│   │   │   │   └── package.json
│   │   │   ├── exports-path
│   │   │   │   ├── cjs.js
│   │   │   │   ├── deep.js
│   │   │   │   ├── dir
│   │   │   │   │   └── dir.js
│   │   │   │   ├── main.js
│   │   │   │   └── package.json
│   │   │   ├── index.html
│   │   │   ├── inline-package
│   │   │   │   ├── inline.js
│   │   │   │   └── package.json
│   │   │   ├── package.json
│   │   │   ├── ts-extension
│   │   │   │   ├── hello.ts
│   │   │   │   └── index.ts
│   │   │   ├── utf8-bom
│   │   │   │   └── main.js
│   │   │   ├── util
│   │   │   │   ├── bar.util.js
│   │   │   │   └── index.js
│   │   │   └── vite.config.js
│   │   ├── resolve-config
│   │   │   ├── __tests__
│   │   │   │   ├── resolve-config.spec.ts
│   │   │   │   └── serve.js
│   │   │   ├── package.json
│   │   │   └── root
│   │   │       ├── index.js
│   │   │       └── vite.config.js
│   │   ├── resolve-linked
│   │   │   ├── dep.js
│   │   │   ├── package.json
│   │   │   └── src
│   │   │       └── index.js
│   │   ├── shims.d.ts
│   │   ├── ssr-deps
│   │   │   ├── __tests__
│   │   │   │   ├── serve.js
│   │   │   │   └── ssr-deps.spec.ts
│   │   │   ├── define-properties-exports
│   │   │   │   ├── index.js
│   │   │   │   └── package.json
│   │   │   ├── define-property-exports
│   │   │   │   ├── index.js
│   │   │   │   └── package.json
│   │   │   ├── forwarded-export
│   │   │   │   ├── index.js
│   │   │   │   └── package.json
│   │   │   ├── index.html
│   │   │   ├── message
│   │   │   ├── object-assigned-exports
│   │   │   │   ├── index.js
│   │   │   │   └── package.json
│   │   │   ├── only-object-assigned-exports
│   │   │   │   ├── index.js
│   │   │   │   └── package.json
│   │   │   ├── package.json
│   │   │   ├── primitive-export
│   │   │   │   ├── index.js
│   │   │   │   └── package.json
│   │   │   ├── read-file-content
│   │   │   │   ├── index.js
│   │   │   │   └── package.json
│   │   │   ├── require-absolute
│   │   │   │   ├── foo.js
│   │   │   │   ├── index.js
│   │   │   │   └── package.json
│   │   │   ├── server.js
│   │   │   ├── src
│   │   │   │   └── app.js
│   │   │   └── ts-transpiled-exports
│   │   │       ├── index.js
│   │   │       └── package.json
│   │   ├── ssr-html
│   │   │   ├── __tests__
│   │   │   │   ├── serve.js
│   │   │   │   └── ssr-html.spec.ts
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── server.js
│   │   │   └── src
│   │   │       └── app.js
│   │   ├── ssr-pug
│   │   │   ├── __tests__
│   │   │   │   ├── serve.js
│   │   │   │   └── ssr-pug.spec.ts
│   │   │   ├── index.pug
│   │   │   ├── package.json
│   │   │   ├── server.js
│   │   │   └── src
│   │   │       └── app.js
│   │   ├── ssr-react
│   │   │   ├── __tests__
│   │   │   │   ├── serve.js
│   │   │   │   └── ssr-react.spec.ts
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── prerender.js
│   │   │   ├── server.js
│   │   │   ├── src
│   │   │   │   ├── App.jsx
│   │   │   │   ├── add.js
│   │   │   │   ├── circular-dep-init
│   │   │   │   │   ├── README.md
│   │   │   │   │   ├── circular-dep-init.js
│   │   │   │   │   ├── module-a.js
│   │   │   │   │   └── module-b.js
│   │   │   │   ├── entry-client.jsx
│   │   │   │   ├── entry-server.jsx
│   │   │   │   ├── forked-deadlock
│   │   │   │   │   ├── README.md
│   │   │   │   │   ├── common-module.js
│   │   │   │   │   ├── deadlock-fuse-module.js
│   │   │   │   │   ├── fuse-stuck-bridge-module.js
│   │   │   │   │   ├── middle-module.js
│   │   │   │   │   └── stuck-module.js
│   │   │   │   ├── multiply.js
│   │   │   │   └── pages
│   │   │   │       ├── About.jsx
│   │   │   │       ├── Env.jsx
│   │   │   │       └── Home.jsx
│   │   │   └── vite.config.js
│   │   ├── ssr-vue
│   │   │   ├── __tests__
│   │   │   │   ├── serve.js
│   │   │   │   └── ssr-vue.spec.ts
│   │   │   ├── dep-import-type
│   │   │   │   ├── deep
│   │   │   │   │   └── index.d.ts
│   │   │   │   └── package.json
│   │   │   ├── example-external-component
│   │   │   │   ├── ExampleExternalComponent.vue
│   │   │   │   ├── index.js
│   │   │   │   └── package.json
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── prerender.js
│   │   │   ├── server.js
│   │   │   ├── src
│   │   │   │   ├── App.vue
│   │   │   │   ├── assets
│   │   │   │   │   ├── button.css
│   │   │   │   │   ├── fonts
│   │   │   │   │   │   ├── Inter-Italic.woff
│   │   │   │   │   │   └── Inter-Italic.woff2
│   │   │   │   │   └── logo.png
│   │   │   │   ├── components
│   │   │   │   │   ├── Foo.jsx
│   │   │   │   │   ├── ImportType.vue
│   │   │   │   │   ├── button.js
│   │   │   │   │   └── foo.css
│   │   │   │   ├── entry-client.js
│   │   │   │   ├── entry-server.js
│   │   │   │   ├── main.js
│   │   │   │   ├── pages
│   │   │   │   │   ├── About.vue
│   │   │   │   │   ├── External.vue
│   │   │   │   │   ├── Home.vue
│   │   │   │   │   └── Store.vue
│   │   │   │   └── router.js
│   │   │   ├── vite.config.js
│   │   │   └── vite.config.noexternal.js
│   │   ├── ssr-webworker
│   │   │   ├── __tests__
│   │   │   │   ├── serve.js
│   │   │   │   └── ssr-webworker.spec.ts
│   │   │   ├── package.json
│   │   │   ├── src
│   │   │   │   └── entry-worker.jsx
│   │   │   ├── vite.config.js
│   │   │   └── worker.js
│   │   ├── tailwind
│   │   │   ├── __test__
│   │   │   │   └── tailwind.spec.ts
│   │   │   ├── index.css
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── postcss.config.js
│   │   │   ├── public
│   │   │   │   └── favicon.ico
│   │   │   ├── src
│   │   │   │   ├── App.vue
│   │   │   │   ├── assets
│   │   │   │   │   └── logo.png
│   │   │   │   ├── components
│   │   │   │   │   └── HelloWorld.vue
│   │   │   │   ├── main.js
│   │   │   │   ├── router.ts
│   │   │   │   └── views
│   │   │   │       └── Page.vue
│   │   │   ├── tailwind.config.js
│   │   │   └── vite.config.ts
│   │   ├── testUtils.ts
│   │   ├── tsconfig-json
│   │   │   ├── __tests__
│   │   │   │   └── tsconfig-json.spec.ts
│   │   │   ├── index.html
│   │   │   ├── nested
│   │   │   │   ├── main.ts
│   │   │   │   ├── not-used-type.ts
│   │   │   │   └── tsconfig.json
│   │   │   ├── nested-with-extends
│   │   │   │   ├── main.ts
│   │   │   │   ├── not-used-type.ts
│   │   │   │   └── tsconfig.json
│   │   │   ├── package.json
│   │   │   ├── src
│   │   │   │   ├── main.ts
│   │   │   │   └── not-used-type.ts
│   │   │   └── tsconfig.json
│   │   ├── tsconfig-json-load-error
│   │   │   ├── __tests__
│   │   │   │   └── tsconfig-json-load-error.spec.ts
│   │   │   ├── has-error
│   │   │   │   ├── main.ts
│   │   │   │   └── tsconfig.json
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── src
│   │   │   │   └── main.ts
│   │   │   └── tsconfig.json
│   │   ├── tsconfig.json
│   │   ├── vue
│   │   │   ├── Assets.vue
│   │   │   ├── AsyncComponent.vue
│   │   │   ├── CssModules.vue
│   │   │   ├── CustomBlock.vue
│   │   │   ├── CustomBlockPlugin.ts
│   │   │   ├── CustomElement.ce.vue
│   │   │   ├── Hmr.vue
│   │   │   ├── Main.vue
│   │   │   ├── Node.vue
│   │   │   ├── PreProcessors.vue
│   │   │   ├── ReactivityTransform.vue
│   │   │   ├── ScanDep.vue
│   │   │   ├── Slotted.vue
│   │   │   ├── Syntax.vue
│   │   │   ├── __tests__
│   │   │   │   └── vue.spec.ts
│   │   │   ├── assets
│   │   │   │   ├── asset.png
│   │   │   │   └── fragment.svg
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── public
│   │   │   │   └── icon.png
│   │   │   ├── setup-import-template
│   │   │   │   ├── SetupImportTemplate.vue
│   │   │   │   └── template.html
│   │   │   ├── src-import
│   │   │   │   ├── SrcImport.vue
│   │   │   │   ├── script.ts
│   │   │   │   ├── srcImportStyle.vue
│   │   │   │   ├── srcImportStyle2.vue
│   │   │   │   ├── style.css
│   │   │   │   ├── style2.css
│   │   │   │   └── template.html
│   │   │   └── vite.config.ts
│   │   ├── vue-jsx
│   │   │   ├── Comp.tsx
│   │   │   ├── Comps.jsx
│   │   │   ├── OtherExt.tesx
│   │   │   ├── Script.vue
│   │   │   ├── SrcImport.jsx
│   │   │   ├── SrcImport.vue
│   │   │   ├── __tests__
│   │   │   │   └── vue-jsx.spec.ts
│   │   │   ├── index.html
│   │   │   ├── main.jsx
│   │   │   ├── package.json
│   │   │   └── vite.config.js
│   │   ├── vue-lib
│   │   │   ├── __tests__
│   │   │   │   ├── serve.js
│   │   │   │   └── vue-lib.spec.ts
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── src-consumer
│   │   │   │   └── index.ts
│   │   │   ├── src-lib
│   │   │   │   ├── CompA.vue
│   │   │   │   ├── CompB.vue
│   │   │   │   └── index.ts
│   │   │   ├── vite.config.consumer.ts
│   │   │   └── vite.config.lib.ts
│   │   ├── wasm
│   │   │   ├── __tests__
│   │   │   │   └── wasm.spec.ts
│   │   │   ├── heavy.wasm
│   │   │   ├── heavy.wasm.map
│   │   │   ├── index.html
│   │   │   ├── light.wasm
│   │   │   └── package.json
│   │   └── worker
│   │       ├── __tests__
│   │       │   └── worker.spec.ts
│   │       ├── index.html
│   │       ├── my-shared-worker.ts
│   │       ├── my-worker.ts
│   │       ├── newUrl
│   │       │   ├── module.js
│   │       │   ├── url-shared-worker.js
│   │       │   └── url-worker.js
│   │       ├── package.json
│   │       ├── possible-ts-output-worker.mjs
│   │       ├── test-plugin.tsx
│   │       ├── vite.config.ts
│   │       └── workerImport.js
│   ├── plugin-legacy
│   │   ├── CHANGELOG.md
│   │   ├── README.md
│   │   ├── index.d.ts
│   │   ├── index.js
│   │   └── package.json
│   ├── plugin-react
│   │   ├── CHANGELOG.md
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── api-extractor.json
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── babel.d.ts
│   │   │   ├── fast-refresh.ts
│   │   │   ├── index.ts
│   │   │   └── jsx-runtime
│   │   │       ├── babel-import-to-require.ts
│   │   │       ├── babel-restore-jsx.spec.ts
│   │   │       ├── babel-restore-jsx.ts
│   │   │       └── restore-jsx.ts
│   │   └── tsconfig.json
│   ├── plugin-vue
│   │   ├── CHANGELOG.md
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── api-extractor.json
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── compiler.ts
│   │   │   ├── handleHotUpdate.ts
│   │   │   ├── helper.ts
│   │   │   ├── index.ts
│   │   │   ├── main.ts
│   │   │   ├── script.ts
│   │   │   ├── style.ts
│   │   │   ├── template.ts
│   │   │   └── utils
│   │   │       ├── descriptorCache.ts
│   │   │       ├── error.ts
│   │   │       └── query.ts
│   │   └── tsconfig.json
│   ├── plugin-vue-jsx
│   │   ├── CHANGELOG.md
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── index.d.ts
│   │   ├── index.js
│   │   └── package.json
│   └── vite
│       ├── CHANGELOG.md
│       ├── LICENSE.md
│       ├── README.md
│       ├── api-extractor.json
│       ├── bin
│       │   ├── openChrome.applescript
│       │   └── vite.js
│       ├── client.d.ts
│       ├── package.json
│       ├── rollup.config.js
│       ├── scripts
│       │   └── patchTypes.ts
│       ├── src
│       │   ├── client
│       │   │   ├── client.ts
│       │   │   ├── env.ts
│       │   │   ├── overlay.ts
│       │   │   └── tsconfig.json
│       │   └── node
│       │       ├── __tests__
│       │       │   ├── asset.spec.ts
│       │       │   ├── build.spec.ts
│       │       │   ├── config.spec.ts
│       │       │   ├── dev.spec.ts
│       │       │   ├── packages
│       │       │   │   ├── name
│       │       │   │   │   └── package.json
│       │       │   │   └── noname
│       │       │   │       └── package.json
│       │       │   ├── plugins
│       │       │   │   ├── css.spec.ts
│       │       │   │   └── import.spec.ts
│       │       │   ├── scan.spec.ts
│       │       │   └── utils.spec.ts
│       │       ├── build.ts
│       │       ├── certificate.ts
│       │       ├── cli.ts
│       │       ├── config.ts
│       │       ├── constants.ts
│       │       ├── http.ts
│       │       ├── importGlob.ts
│       │       ├── index.ts
│       │       ├── logger.ts
│       │       ├── optimizer
│       │       │   ├── esbuildDepPlugin.ts
│       │       │   ├── index.ts
│       │       │   ├── registerMissing.ts
│       │       │   └── scan.ts
│       │       ├── packages.ts
│       │       ├── plugin.ts
│       │       ├── plugins
│       │       │   ├── asset.ts
│       │       │   ├── assetImportMetaUrl.ts
│       │       │   ├── clientInjections.ts
│       │       │   ├── css.ts
│       │       │   ├── dataUri.ts
│       │       │   ├── define.ts
│       │       │   ├── esbuild.ts
│       │       │   ├── html.ts
│       │       │   ├── importAnalysis.ts
│       │       │   ├── importAnalysisBuild.ts
│       │       │   ├── index.ts
│       │       │   ├── json.ts
│       │       │   ├── loadFallback.ts
│       │       │   ├── manifest.ts
│       │       │   ├── modulePreloadPolyfill.ts
│       │       │   ├── preAlias.ts
│       │       │   ├── reporter.ts
│       │       │   ├── resolve.ts
│       │       │   ├── ssrRequireHook.ts
│       │       │   ├── terser.ts
│       │       │   ├── wasm.ts
│       │       │   ├── worker.ts
│       │       │   └── workerImportMetaUrl.ts
│       │       ├── preview.ts
│       │       ├── server
│       │       │   ├── __tests__
│       │       │   │   ├── fixtures
│       │       │   │   │   ├── lerna
│       │       │   │   │   │   ├── lerna.json
│       │       │   │   │   │   └── nested
│       │       │   │   │   │       └── package.json
│       │       │   │   │   ├── none
│       │       │   │   │   │   └── nested
│       │       │   │   │   │       └── package.json
│       │       │   │   │   ├── pnpm
│       │       │   │   │   │   ├── nested
│       │       │   │   │   │   │   └── package.json
│       │       │   │   │   │   ├── package.json
│       │       │   │   │   │   └── pnpm-workspace.yaml
│       │       │   │   │   └── yarn
│       │       │   │   │       ├── nested
│       │       │   │   │       │   └── package.json
│       │       │   │   │       └── package.json
│       │       │   │   ├── pluginContainer.spec.ts
│       │       │   │   └── search-root.spec.ts
│       │       │   ├── hmr.ts
│       │       │   ├── index.ts
│       │       │   ├── middlewares
│       │       │   │   ├── base.ts
│       │       │   │   ├── error.ts
│       │       │   │   ├── indexHtml.ts
│       │       │   │   ├── proxy.ts
│       │       │   │   ├── spaFallback.ts
│       │       │   │   ├── static.ts
│       │       │   │   ├── time.ts
│       │       │   │   └── transform.ts
│       │       │   ├── moduleGraph.ts
│       │       │   ├── openBrowser.ts
│       │       │   ├── pluginContainer.ts
│       │       │   ├── searchRoot.ts
│       │       │   ├── send.ts
│       │       │   ├── sourcemap.ts
│       │       │   ├── transformRequest.ts
│       │       │   └── ws.ts
│       │       ├── ssr
│       │       │   ├── __tests__
│       │       │   │   └── ssrTransform.spec.ts
│       │       │   ├── ssrExternal.ts
│       │       │   ├── ssrManifestPlugin.ts
│       │       │   ├── ssrModuleLoader.ts
│       │       │   ├── ssrStacktrace.ts
│       │       │   └── ssrTransform.ts
│       │       ├── tsconfig.json
│       │       └── utils.ts
│       ├── tsconfig.base.json
│       └── types
│           ├── alias.d.ts
│           ├── anymatch.d.ts
│           ├── chokidar.d.ts
│           ├── commonjs.d.ts
│           ├── connect.d.ts
│           ├── customEvent.d.ts
│           ├── dynamicImportVars.d.ts
│           ├── hmrPayload.d.ts
│           ├── http-proxy.d.ts
│           ├── importMeta.d.ts
│           ├── package.json
│           ├── shims.d.ts
│           ├── terser.d.ts
│           └── ws.d.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── scripts
│   ├── jestEnv.cjs
│   ├── jestGlobalSetup.cjs
│   ├── jestGlobalTeardown.cjs
│   ├── jestPerTestSetup.ts
│   ├── patchEsbuildDist.ts
│   ├── patchFileDeps.ts
│   ├── publishCI.ts
│   ├── release.ts
│   ├── releaseUtils.ts
│   ├── tsconfig.json
│   └── verifyCommit.ts
└── tree.txt

298 directories, 969 files

  • 298 directories, 969 files
  • packages/playground 配下が大きい
    • 215 directories, 628 files を占める
    • 様々な環境でViteを利用するミニマムセットで、全てテストを含んでいる
    • playgroundgrepしてみると、 CONTRIBUTING.md がヒットし、テストに関する記述などが出てくる
    • jestの設定に含まれており、 playwright-chromium 経由で Playwright を起動してブラウザ上でテストが実行される
  • packages/vite がvite本体ぽい
    • 27 directories, 114 files
    • コア部分は意外と小さい

ディレクトリだけ一覧して眺める

ファイル一覧だと Playground 配下が巨大すぎてアレなので、ディレクトリだけで一覧してみる。

  • tree -d
  • playground node_modules は除去

という感じで出力してみた。

.
├── docs
│   ├── blog
│   ├── config
│   ├── guide
│   ├── images
│   ├── plugins
│   └── public
├── packages
│   ├── create-vite
│   │   ├── __tests__
│   │   ├── template-lit
│   │   │   └── src
│   │   ├── template-lit-ts
│   │   │   └── src
│   │   ├── template-preact
│   │   │   └── src
│   │   ├── template-preact-ts
│   │   │   └── src
│   │   ├── template-react
│   │   │   └── src
│   │   ├── template-react-ts
│   │   │   └── src
│   │   ├── template-svelte
│   │   │   ├── public
│   │   │   └── src
│   │   │       ├── assets
│   │   │       └── lib
│   │   ├── template-svelte-ts
│   │   │   ├── public
│   │   │   └── src
│   │   │       ├── assets
│   │   │       └── lib
│   │   ├── template-vanilla
│   │   ├── template-vanilla-ts
│   │   │   └── src
│   │   ├── template-vue
│   │   │   ├── public
│   │   │   └── src
│   │   │       ├── assets
│   │   │       └── components
│   │   └── template-vue-ts
│   │       ├── public
│   │       └── src
│   │           ├── assets
│   │           └── components
│   ├── playground
│   │   └── (※省略)
│   ├── plugin-legacy
│   ├── plugin-react
│   │   ├── dist
│   │   └── src
│   │       └── jsx-runtime
│   ├── plugin-vue
│   │   ├── dist
│   │   └── src
│   │       └── utils
│   ├── plugin-vue-jsx
│   └── vite
│       ├── bin
│       ├── dist
│       │   ├── client
│       │   └── node
│       │       └── chunks
│       ├── scripts
│       ├── src
│       │   ├── client
│       │   └── node
│       │       ├── __tests__
│       │       │   ├── packages
│       │       │   │   ├── name
│       │       │   │   └── noname
│       │       │   └── plugins
│       │       ├── optimizer
│       │       ├── plugins
│       │       ├── server
│       │       │   ├── __tests__
│       │       │   │   └── fixtures
│       │       │   │       ├── lerna
│       │       │   │       │   └── nested
│       │       │   │       ├── none
│       │       │   │       │   └── nested
│       │       │   │       ├── pnpm
│       │       │   │       │   └── nested
│       │       │   │       └── yarn
│       │       │   │           └── nested
│       │       │   └── middlewares
│       │       └── ssr
│       │           └── __tests__
│       └── types
└── scripts
  • そこまで階層が深いわけでもなく、わりとシンプルなディレクトリ構造な印象
  • create-vite vite の2つがコアって感じ

テストを動かしてみる

pnpm i
pnpm test
  • 普通に大量に落ちる。なんでや!
  • Target page, context or browser has been closed
  • https://github.com/vitejs/vite/issues/4543 1回ビルドしないとダメぽい
  • pnpm build 実行 → まだ落ちる

Since Jest will attempt to run tests in parallel, if your machine has many cores this may cause flaky test failures with multiple Playwright instances running at the same time. You can force the tests to run in series with pnpm run test-serve -- --runInBand.

  • らしいので、 pnpm run test-serve -- --runInBand を試したところパスした

とりあえずディレクトリを眺めてテストを通すところまでやった。

最初に全ファイル出力したときは「おおぅ...」となったが、playground系のコードを省いたらわりとなんとかなる気がしてきた。

次→ルートにある各種ファイルを見てみる

サイボウズのフロントエンドエキスパートチームに入った

今月からサイボウズに入社し、フロントエンドエキスパートチームのメンバーになった。

有休消化期間&GWでたっぷり眠る生活を続けていたので社会復帰できるか心配だったが、なんとかなってる。

blog.cybozu.io

よくある感じで数ヶ月は試用期間扱いで、それが終わってからブログに書こうかなと思っていた。ただ、試用期間中所属を隠すとなると、チームの活動内容のひとつに「発信」があるにも関わらず数ヶ月間なにも発信しない気かお前という話になってしまうし、そもそもそんな気持ちで仕事するの良くないよなという考えに至り、書いちゃうことにした。

なぜ

チームの存在は以前から知っていて、特にYouTubeでやってるフロントエンドマンスリーはよく観ていた。

最初は「ほ〜ん、すごい人たちがおるもんやな〜」ぐらいに思ってたのだが、フロントエンドを探究・研鑽しつつレガシーを改善するというテーマにも立ち向かっているのを知り、個人的にも本を出したくらいにはフロントエンドの脱レガシーには思い入れはあったので、なんとなく通ずるものは感じていた。

同時に自分自身は「このままだと良くないなぁ」という漠然とした不安みたいなものがあり、今思えばコンフォートゾーンってものへの危機感だったのかもしれない。

ぼんやりと「一緒に仕事できたら何か少しは役に立てる部分があるかもしれない」「つよいチームなので自分も成長できそう」「とはいえ自分で通用するのかな」とかを考えていたころ、偶然にも元同僚の身内に社員の方がいることを教えてもらい、その方に繋いでもらってカジュアル面談を受けることができた。本当に超絶感謝しています。

カジュアル面談にチームのほぼ全員が現れたのにはビビったが、採用に対してチームで真摯に向き合ってる証拠だと思えたし、実際に話せたことで想像していた以上に魅力に感じた。

そのあとは採用に応募し、なんやかんやあって今にいたる。

なお個人的に5年以上地域RubyコミュニティであるToyama.rbを運営しており、Rubyコミュニティ主催者がRuby書かなくてええんかという話だが、冷静に考えると自分はここ1年ぐらいのもくもく会ではRustとJSしかやってないし、そもそも内容を縛っているコミュニティではない。RubyRailsも今でも好きだから問題ない。問題ない!!!

入ってみてどうか

社風

まずサイボウズという会社の風土にいい意味で驚くことが多い。自由度が高く、現場の裁量がとても大きい。これだけの規模の会社でこれを実現できているのは衝撃を受ける。ここまで辿り着くまでいったいどれだけの血と汗と涙が流れたのだろう..。

ただ、自由であることは同時に各々が考えと責任を持って行動する必要があるため、ぼんやりと決められた仕事をするのを期待していると死ぬことになりそう。

チーム

フロントエンドエキスパートチームについては、予想通り「すごい人たちの集まりやでぇ..!!」となってる。各々が何かしらで特出した領域を持っている印象。そしてみんな良い人。

チームの活動範囲は広く、組織横断的なチームなので関わるプロダクトも多い。そして社風もそうだが、チーム特性上さらに自由度が高く、何らかの目的意識を見つけて自主的に動いていく必要がある。

今のところはまだ特に何も色を出せてない焦りを早くも感じるが、自分は地に足をつけて泥臭い改善作業を時間かけて倒すみたいなところが力を出せる部分だと理解しているので、焦るよりも少し長いスパンで考えて成果に繋げることができたらいいなぁと思う。ひとまずは今までやってきたことを信じて頑張っていきたい。

今後

この機会なので今までやれてなかったことも取り組みたい。OSS活動と英語かなと思う。特に英語は信じられないぐらい貧弱なのでやばい。社内制度使ってレアジョブを始めることにしたので、ちゃんとやるぞ。

ちなみに

すぐにサイボウズに応募しようと決めたわけではなく、数ヶ月間で多くの企業のカジュアル面談を受けた。ご対応頂いた企業様・担当者様ありがとうございました。

あらためて、リモートワーク可能な会社がめっちゃ増えたんだなぁと実感した。

4年半前に転職活動をしたときは、富山からフルリモート可能な会社を探すのは非常に困難だった覚えがある。コロナによる影響だとは思うが、地方在住勢にとっては選択肢が増えるのは人生の幅が広がるのでとてもありがたい。4年半前に同じ転職が可能だったか?と言われると自信はない。

自分への戒めとして、地方に住んでいることを言い訳にすることがないように今後も頑張っていきたい。

おわり。

雑談用Shownoteの作成・共有サービスをつくった

チュートリアルじゃなくて、ちゃんと何か作って試しておかないとな〜という技術がたくさん自分の中で累積していたので、次のようなサービスを作って公開した。

shownotes.vercel.app

概要

雑談・1on1・リモート飲み会・勉強会などで、話したいネタを事前に作成・共有しておき、会話時には再生する形で1つずつネタを提示してくれる。

いまのところShownote自体の作成はGoogleログインを必要としているが、作ったShownoteの閲覧や雑談トピックの追加みたいなところはURLさえ知ってれば誰でもオッケーにしてる。

ログインしたらボタンを押せばShownoteの雛形が作れる。

f:id:mugi1:20210505225118p:plain

Shownote自体には自由に雑談テーマを追加できる。これはURLさえ知ってれば誰でもできるので、一緒に話したい人に事前共有しておける。

f:id:mugi1:20210505225358p:plain

実際に話すときは、追加順 or ランダム再生で、1個ずつトピックを表示してくれる。

f:id:mugi1:20210505225707p:plain

一応一定間隔でPollingしてるので、同じ画面を見ている人では表示が同期されるようになってる。5秒ほど開けてるので、同時に見ると少しラグとかズレがあるのは勘弁してほしい。websocketとか使えば良かったのだが、あんまりそのへんで頑張りたくなかったので雑にApolloClientでPollingとした。

f:id:mugi1:20210505230611g:plain
Pollingでなんとか同期している図

話してる時間をそれっぽく計測しているので、最後に一番盛り上がったトークテーマがどれだったかとかが表示される。

f:id:mugi1:20210505230454p:plain

なお一応公開しているが、使う人がいるかはわからなかったので現状以下の制約がある。

  • PostgreSQLがHerokuのフリープランなので10000レコードでサービスが死ぬ
  • アプリ自体がVercelにデプロイしてるので、DB接続で若干ラグがある

もし使う人がいるようであれば追々解消させる予定だが、誰もいなければ何もしない。

使った技術要素とそれらへの所感

次のようなものを使ってる

3画面しかないのに完全にオーバーキル構成である。 実現機能を考えればどう考えてもFirestoreとか使った方が10000000%良いのは最初からわかっていたが、これは仕事ではないので、そんなことより自分が勉強目的に使ってみたいモチベーションで使いたいものを全部入れた。問題はない。

Next.js

かなり昔のバージョンでチュートリアル的なことはやったことがあったが、改めてちゃんと何かを作ったのは始めてかもしれない。

普段Vueに慣れ親しんでいた身としては、ReactというだけでTS親和性が高くてとても良かった。また、next/image での画像最適化など、痒いところに手が届く感もあり、Vercelを使ったのでデプロイの簡単さも抜群だった。

一応部分的にSSRも採用したが、これは実際にプロダクション導入する場合は慎重に検討すべきところだな〜と改めて感じた。 Next.js側自体の推奨としても、認証が必要なページでのSSRとかはいらないよね??みたいな話もあったりするし、どうしても複雑化するので、サービスとして何を重視するのかとちゃんと向き合うのが最初にすべきことかもしれない

とはいえ、トータルの開発体験としてはとても素晴らしいものだった。新規で何か作るなら積極的に採用するよなぁというのも頷ける。

Prisma

今回このサービスを作った発端のモチベーションは「Prisma使って何か作ってみたい!!!」だったので、強引にPrismaを採用した。簡単に説明すると、Node.jsで動くORMです。

最近まで仕事でRailsを使っていて、今もRailsは好きなのだが、何が好きかというとActiveRecordの操作感に尽きるな〜と思ってる。(色々細かいことやりだすと大変なんだけども)

PrismaでのDB操作はかなりそれに近い感覚があり、prisma.user.findFirst(...) のような形でデータを取得できる。そして何よりも、それらを全てTypeScriptで型安全に操作できるのがとても良い。

また、現在はPrismaにはMigrationの仕組みが備わっていて、全体の主軸となる *.prisma に書かれたスキーマ定義をもとに、変更時には差分をMigrationとして抽出することができる。今回のような趣味レベルで使った範囲では特に困ることもなく動作したし、Migrationファイルの内容もただのALTER文が書かれているだけなので、SQL読める人なら内容も簡単に理解できて、編集しようと思えば編集できる。

全体的にかなり使いやすい印象。今後大きく流行っても全然不思議じゃない。

Nexus

APIはGraphQLにしたが、バックエンドにはNexusを使ってみた。

GraphQLでAPIを構築する際には Schema First か Code First の選択肢があるが、Nexusは Code Firstのフレームワークに該当する。

Schema First と Code First のどちらを良しとするかはサービスの規模感やチームの方針によって変わるため一概には判断できないと思うが、個人的には Code First のほうが全体の記述量が減ることが多いので好み。

ただ、Nexusにしたのは好みだけが理由ではなく、Prismaとの相性が良いのもある。 nexus-plugin-prismaというNexus用のプラグインが存在しており、たとえば「Bookモデルの中身を返すクエリ」を作るときに、Prismaが生成しているモデル情報をもとにレスポンスを定義することができる。

これの何が嬉しいかというと、DBが変わるとそれに伴ってNexusが生成するGraphQLスキーマも変化するので、そこからGraphQL Code Generatorを通せばクライアント側での型不整合なども全部チェックできるようになる。

なお、DBモデルをそのままクエリとして露呈するとか危ないでしょって最初は思ったが、公開するフィールドは明示的に指定したもののみに限定されるため、意図せず公開されるみたいなことは無く、そのあたりも問題なかった。

DB⇄APIサーバー⇄クライアントの全てが型定義で一貫してチェックされるのはとても快適で、DB変更時にクライアント側の変更が漏れたりするようなケースは開発中に一度も無かった。

Redux (Redux Toolkit)

Reduxをちゃんと使ったのは5年くらい前で、とてもツラかったような記憶があったのだが、Hooks対応なども経てとても良くなったと聞いたので試してみた。

結果としてはかなり使いやすくなってた。Redux Toolkitがとても良かった。昔Reduxでストレスに感じてたのは、やりたいことに対して手数があまりにも多すぎる点だったが、そのあたりが綺麗さっぱり解消されてる感覚があった。

これだけ使いやすいなら、下手にオレオレ状態管理するよりかは、何も考えずにとりあえずRedux採用したほうがトータルのコストは下がりそう。

まとめ

チュートリアルだけ過去にやった技術も多かったが、実際に小さいアプリケーションを作ってみることで得られる知見はとても多かった。モノを作って学ぶのは大事だな〜と改めて感じた。

ちなみに、今回のサービスのアイデアとしては「Clubhouseでのアジェンダ共有ができるといいかな〜」というのがあったが、先にClubhouseが瀕死になってしまった。

とはいえ中途半端で終わるのは悲しいので気合いで作り切った。完走するのは大事。

サービスとしては公開しておくので興味があれば使ってみてください。 https://shownotes.vercel.app/

不具合とか機能要望とかあれば @mugi_uno まで。

おわり

退職

退職

正確にはまだ在籍中だが、約4年在籍していたMisoca(→今は合併して弥生株式会社)を2021年4月いっぱいで退職する。4/16が最終出社日だった。

いま思い返すと色々やったなぁという感じ。主にフロントエンドのモダン化に力を注いでいた。とはいえ、バックエンドも日常的に触ってたし、採用に関わらせてもらう機会もあった。貴重な経験を多く積めて、考え方的な部分では今後一生使えるものを学ぶことができたと思う。Misocaで働けて本当によかった。

大変お世話になりました。ありがとうございました。

なぜ

フロントエンドを仕事でやっていて、改めておもしろいな〜と感じるようになり、もっとフロントエンドに浸かれる環境で仕事をしたいと考えたのと同時に、研鑽できるよう、さらにチャレンジできるような環境に身を置きたくなった。また、今までやってきたことが、さらに大きい規模でも通用するのか試してみたい気持ちもある。

そういうわけで、5月からは某社のフロントエンドチームでどっぷりフロントエンドをやっていく予定です。

以前から遠目で見てすごいな〜って思っていた方々と一緒に働くことになるので、 自分がついていけるのか不安になってますが、これまでの経験をもとに頑張りたい。


そういうわけで例のリスト置いておきます。

https://www.amazon.jp/hz/wishlist/ls/29597IU52BIZ6?ref_=wl_share

RBSからTypeScriptに変換するGem (rbs2ts) を作ってる

Ruby3.0 からは、型定義を処理するための rbs gem が同梱されていて、これは外部の *.rbs ファイルに記述した内容に従って、Rubyコードの型チェックを可能にしてくれる。

github.com

最近、この RBS の型定義を TypeScript の型定義に変換できないかな〜と思い、 rbs2ts という gem を実験的に作ってる。

結構荒削りなので、細々した部分での挙動は正直怪しいが、ある程度それっぽく動くようになったので公開してある。

rubygems.org

github.com

Gemのいまのところの挙動

いまのところ次のような変換ができる

Alias

RBS

type TypeofInteger = Integer
type TypeofFloat = Float
type TypeofNumeric = Numeric
type TypeofString = String
type TypeofBool = Bool
type TypeofVoid = void
type TypeofUntyped = untyped
type TypeofNil = nil

変換後TypeScript

export type TypeofInteger = number;

export type TypeofFloat = number;

export type TypeofNumeric = number;

export type TypeofString = string;

export type TypeofBool = boolean;

export type TypeofVoid = void;

export type TypeofUntyped = any;

export type TypeofNil = null;

リテラル

RBS

type IntegerLiteral = 123
type StringLiteral = 'abc'
type TrueLiteral = true
type FalseLiteral = false

変換後TypeScript

export type IntegerLiteral = 123;

export type StringLiteral = "abc";

export type TrueLiteral = true;

export type FalseLiteral = false;

Intersection, Union

RBS

type IntersectionType = String & Integer & Bool
type UnionType = String | Integer | Bool

変換後TypeScript

export type IntersectionType = string & number & boolean;

export type UnionType = string | number | boolean;

Optional

RBS

type OptionalType = String?

変換後TypeScript

export type OptionalType = string | null | undefined;

Array, Tuple

type ArrayType = Array[String]

type TupleType = [ ]

type TupleEmptyType = [String, Integer]

変換後TypeScript

export type ArrayType = string[];

export type TupleType = [];

export type TupleEmptyType = [string, number];

Record

RBS

type RecordType = {
  s: String,
  nest: {
    i: Integer,
    f: Float
  }?
}

変換後TypeScript

export type RecordType = {
  s: string;
  next: {
    i: number;
    f: number;
  } | null | undefined;
};

クラス

クラスっていうかメソッド。これが一番やばい。 これであってるのかホントに

RBS

class Klass
  attr_accessor a: String
  attr_reader b: Integer
  attr_writer c: Bool

  def required_positional: (String) -> void
  def required_positional_name: (String str) -> void
  def optional_positional: (?String) -> void
  def optional_positional_name: (?String? str) -> void
  def rest_positional: (*String) -> void
  def rest_positional_name: (*String str) -> void
  def rest_positional_with_trailing: (*String, Integer) -> void
  def rest_positional_name_with_trailing: (*String str, Integer trailing) -> void
  def required_keyword: (str: String) -> void
  def optional_keyword: (?str: String?) -> void
  def rest_keywords: (**String) -> void
  def rest_keywords_name: (**String rest) -> void
end

変換後TypeScript

export declare class Klass {
  a: string;
  readonly b: number;
  c: boolean;
  requiredPositional(arg1: string): void;
  requiredPositionalName(str: string): void;
  optionalPositional(arg1?: string): void;
  optionalPositionalName(str?: string | null | undefined): void;
  restPositional(...arg1: string[]): void;
  restPositionalName(...str: string[]): void;
  restPositionalWithTrailing(arg1: string[], arg2: number): void;
  restPositionalNameWithTrailing(str: string[], trailing: number): void;
  requiredKeyword(arg1: { str: string }): void;
  optionalKeyword(arg1: { str?: string | null | undefined }): void;
  restKeywords(arg1: { [key: string]: unknown; }): void;
  restKeywordsName(arg1: { [key: string]: unknown; }): void;
};

モジュール

RBS

module Module
  def func: (String, Integer) -> { str: String, int: Integer }
end

変換後TypeScript

export namespace Module {
  export declare function func(arg1: string, arg2: number): {
    str: string;
    int: number;
  };
};

インタフェース

RBS

interface _Interface
  def func: (String, Integer) -> { str: String, int: Integer }
end

変換後TypeScript

export interface Interface {
  func(arg1: string, arg2: number): {
    str: string;
    int: number;
  };
};

作ろうと思った動機

GraphQL Code Generator べんり!!

最近、ちゃんとGraphQLを使う機会があり、GraphQL Ruby によって出力されたスキーマファイルを元に GraphQL Code Generator で TypeScript 型定義に変更し、それをフロントエンドで利用するスタイルで開発していた。

この体験が非常に良くて、ある程度のバックエンド側の変更であれば、フロントエンド側への影響は TypeScript の型検査で拾うことができ、名前をタイポしてましたみたいな悲しい不具合はほぼ防げてた。

RESTつらい

GraphQLでの型の体験を味わうと、逆に次のようなものをなんとかしたくなってくる。

これらは、フロントエンド側でインタフェースを独自で型定義すれば、受け取った後についてはある程度検査できるが、あくまでも独自定義なので、当然バックエンドの変更に対して自動で追従することはできなくて、人力でなんとかする必要がある。

GraphQL以外の方法

全部GraphQLに置き換えちゃえばいいじゃん、というのがまず思い浮かぶストレートな解決策なんだけど、まあそれはほら、大変ですよね。

また、別のアプローチとして OpenAPI や gRPC を利用する方法もあるはず。gRPCなんかはそこまで詳しくないので的外れなことを言ってる可能性はあるが、すでに存在するAPI群に対して後追いで適用するには少しハードルが上がるものかな〜と思っている。 もちろんゼロから構築できるのであれば積極的に導入を検討してもよさそう。(楽しそうだし)

今回は、自分自身のリアルな課題への対処として、段階的にかつコスト低く徐々に保護される範囲を広げていく手段がほしいなぁと考えてた。

RBSからいけない?

というわけで、RBSが使えないかな?と思ったのが発端。

REST用のレスポンスはPresenterクラス的なもので構築してたりするので、バックエンドではそれを RBS 型定義でチェックした上で、その RBS から TypeScript の型定義が出力できれば、現実で稼働しているAPIを大幅に変更することなく、型定義の恩恵だけいい感じに受けられるのでは??という発想。

今後

まず自分が使わなければな..と思っている。 ドッグフーディングとは 意味/解説 - シマウマ用語集

一応出力されてる型定義は TypeScript Playground にペタッとしてエラーになってないことは確認してるけど、実際に使い物になるかは本気で使ってみないとわからんなという気持ちになってる。

ともあれ、実は何気にちゃんとRubyGems作るのも初めてなので、結構楽しい。 RBSのsyntaxとかを見るとまだまだサポートできていない部分がたくさんあるので、ちまちま更新していきたい。