📘

viteでTypeScriptのバックエンド開発環境を動かす

2023/01/29に公開

フロントエンドだけでなくバックエンドでもviteで開発環境を動かしたい。

vite-plugin-node

結論から言うと、vite-plugin-node を使えば問題なく動く。注意点としてはesbuildがレガシーデコレータに対応していないので、NestJSでは動かないと考えて良い。

https://github.com/axe-me/vite-plugin-node

リクエストの流れとしては、vite -> backend framework で、viteに送るリクエストをpluginのなかでmiddlewareを使用してインターセプトして、fastifyやexpressなどのバックエンドのフレームワークのinputに合致するようにリクエストを改変して送っている。

自前で実装

ライブラリに依存したくない場合は自前でviteのpluginを実装することとなるが、上記のpluginの実装を簡素化することで少ない行数で実装できる。以下が実装例。fastifyを使用しているが、expressでも頑張れば動くようにすることはできると思われる。

// vite.config.ts
import type http from "node:http";
import { defineConfig, PluginOption, ViteDevServer } from "vite";

const fastifyAdapter = (server: ViteDevServer) => {
  return async (req: http.IncomingMessage, res: http.ServerResponse) => {
    const { app } = await server.ssrLoadModule("./server");

    app.routing(req, res);
  };
};

const FastifyPlugin = {
  name: "fastify-adapter",
  configureServer: (server) => {
    server.middlewares.use(fastifyAdapter(server));
  },
} as const satisfies PluginOption;

export default defineConfig({
  build: {
    target: "es2022",
    rollupOptions: {
      // Suppress a warning when starting up
      input: "./server.ts",
    },
  },
  server: {
    port: 3000,
    hmr: true,
  },
  plugins: [FastifyPlugin],
});

ssrLoadModule("./server")というのが重要で、その名の通りSSRモード時に使用する関数らしいが、これを使用することでimport.meta.envなども設定された上で実行される。以下がssrLoadModuleで読み込むjsファイル。なお、Native ESMでtop-level awaitを使っている。

// server.ts
import fastify from "fastify";
import mercurius from "mercurius";
import { schema } from "./src/schema";

export const app = fastify({ logger: true });

app.register(mercurius, {
  graphiql: true,
  path: "/graphql",
  schema: schema,
});

await app.ready();

https://vitejs.dev/guide/ssr.html

上記の例では、リクエスト毎にssrLoadModuleを呼んでいるが、コードを変えなければキャッシュが使用されるので、2回目のリクエスト以降はfastifyサーバーが再起動することはない。が、問題はコードを変更したときで、ここはHMRが働かないのかサーバーが再起動となる。import.meta.hot を使えるかと思ったが、常にundefinedでアクセスすることができなかった。 これに関しては、vite-node-pluginのREADME.mdでは大丈夫的なことが書いてあるが、実際はよくわからない。ある程度大きいコードベースで試してみたいところ。

You may ask, isn't super slow, since it re-compiles/reloads the entire app? The answer is NO, because vite is smart. It has a builtin module graph as a cache layer, the graph is built up the first time your app loads. After that, when you update a file, vite will only invalidate that one and its parent modules, so that for next request, only those invalidated modules need to be re-compiled which is super fast thanks to esbuild or swc.

https://github.com/axe-me/vite-plugin-node#how

その他

vitestの中にvite-nodeというnpmパッケージがあり、これでも同じことが実現できるかと思ったが、思うようにいかなかった。

https://github.com/vitest-dev/vitest/tree/main/packages/vite-node

2023/02/04追記

vite-nodeで動かないと書いたが、watchオプション追加でコード変更時にlive reloadをしてくれるようになった。これで自前実装のプラグインは消すことができる。

vite-node --watch server.ts
await app.listen({ port: 3000 });

if (import.meta.hot) {
  import.meta.hot.on("vite:beforeFullReload", () => {
    return app.close();
  });
}

以上

Discussion