viteでTypeScriptのバックエンド開発環境を動かす
フロントエンドだけでなくバックエンドでもviteで開発環境を動かしたい。
vite-plugin-node
結論から言うと、vite-plugin-node
を使えば問題なく動く。注意点としてはesbuildがレガシーデコレータに対応していないので、NestJSでは動かないと考えて良い。
リクエストの流れとしては、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();
上記の例では、リクエスト毎に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.
その他
vitestの中にvite-node
というnpmパッケージがあり、これでも同じことが実現できるかと思ったが、思うようにいかなかった。
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