🚀
最強 DX な Server-side TypeScript 開発環境を作ってみた (swc)
What
- 最強の Developer eXperience な Server-side TypeScript 開発環境を作ってみた。
- あまり Server-side TS に詳しくないので、問題などあればご指摘願いたい。
Requirements
粒度バラバラだが、必ず実現したいのはこの辺り。
- TypeScript
- No tsc build (Use swc or esbuild)
- yarn berry (v3)
- VSCode debugger
- Docker compatible
- Hot reload
- Absolute path import
Frontend engineer な人から見れば「何を当たり前のことを」と思うかもしれない。
でもこれらを Server-side Node.js で実現しようとすると意外と壁が多かった。特に NestJS はいろいろ困難。
TL;DR
とりあえずメジャーな express.js で試してみた例。
Fastify でもうまくいくはず。es decorators とかいう負の遺産(やや偏見)をゴリゴリ使ってる NestJS は build できない。
swc config 弄れば対応できるのかもしれないが、今のところ es decorators を使う気はないので試してない。
それぞれどうしているか?
環境は Mac M1。
TypeScript
- type check は普通に tsc
No tsc build (Use swc)
- build は swc。爆速 🚀
yarn berry (v3)
Setup 手順
# asdf を使って必要な runtime を一括 install
cut -d' ' -f1 .tool-versions | grep "^[^\#]" | xargs -I {} asdf plugin add {}
asdf install
# yarn 3 を始める
# see vers list: https://github.com/yarnpkg/berry/releases
yarn set version 3.2.3
# Setup yarn config
# PnP/Zero-Installs はまだ運用が面倒くさそうなので使わない
yarn config set nodeLinker node-modules
# 非ライブラリで not save-exact にするメリットはないので、save-exact を強制する
# yarn berry (v3) では save-prefix option -> defaultSemverRangePrefix になった
# .npmrc の save-exact は無視される
yarn config set defaultSemverRangePrefix ""
# yarn plugin の追加
# yarn v1 の `yarn install --production` 相当のことをしたいため
yarn plugin import workspace-tools
# npm-check-updates 相当のことをしたいため
yarn plugin import interactive-tools
# init
yarn init -p -y
yarn
- 特に、
yarn install --production
が廃止され plugin とやらに移行したのには面食らった。- どうも PnP/Zero-Installs 推しのために plugin へ追いやられたようだが、以下の issue の通り「何してくれとんねん」という感じ。
- ref. [Feature] v2 equivalent of v1's "yarn install --production --frozen-lockfile" · Issue #2253 · yarnpkg/berry
- また、yarn 本体や plugin を commit しないといけないのもモヤっとする。
- .gitattributes を書いて、GitHub で余計な表示がされないよう設定する。
- PnP/Zero-Installs ガッツリ使うなら、Git LFS も必要そう。
あと、asdf はいいぞ。
全部 asdf + .tool-versions で完結するの本当に楽。GitHub Actions の actions/setup-node@v3 も対応している。
VSCode debugger
- .vscode/launch.json の通り。これは特に奇抜なことはなし。
Docker compatible
Dockerfile
# Build container
FROM node:18.9.0 as builder
WORKDIR /work
COPY . .
RUN yarn install --immutable && \
yarn build && \
yarn workspaces focus --all --production
# Runtime container
FROM gcr.io/distroless/nodejs:18
COPY /work/dist /dist
COPY /work/node_modules /node_modules
CMD ["/dist/index.js"]
- multi stage build で Runtime container の image size を減らしている
-
yarn workspaces focus --all --production
で devDependencies の packages を減らし、 -
COPY --from=builder /work/node_modules /node_modules
の copy 量が減り、 - 結果、Runtime container の image size が減る
-
webpack などで single js にすればさらに減らせるのだが、以下の記事の通り問題がありそうなのでやめた。
ref. node.js のメトリクスの計測、ベンチマークの改善、Docker イメージの絞り方を勉強した - mizdev
シングルファイル化は現実的なアプリでは厳しい気がする。ファイルの相対パスが崩れて、他の設定を読む系のものが動かないし、エラーのスタックトレースを追うのが難しくなる。あくまで可能な場合の最終手段という感じ。
Hot reload
package.json
// ...
"scripts": {
"build:cleanup": "rm -rf dist",
"dev:build": "yarn build:cleanup && swc src -d dist --config-file configs/.swcrc.dev",
"dev:start": "nodemon --watch dist --ext js --exec 'node dist/index.js'",
"dev": "yarn dev:build --watch & sleep 3 && yarn dev:start",
// ...
nodemon で hot reload させている
"dev": "yarn dev:build --watch & sleep 3 && yarn dev:start",
- swc --watch で build すると、そのプロセスが終わらない = 後続の
&& yarn dev:start
に進めず起動できない、となってしまう。 - なので、watch は
&
で background 実行させ、初回 build が終わるまで sleep で適当に待たせ、その後起動させてる。
Absolute path import
- .swcrc と tsconfig.json の組合せで実現している。
未解決の問題
- es modules に対応できてない
- lodash-es や ky を import して実行すると、以下のようなエラーになる
- import (ESM) が require (CommonJS) に変換されているので当然といえば当然だが
$ node dist/index.js
/Users/arx/try-server-side-ts/dist/handlers/index.js:10
const _ky = /*#__PURE__*/ _interopRequireDefault(require("ky"));
^
Error [ERR_REQUIRE_ESM]: require() of ES Module /Users/arx/try-server-side-ts/node_modules/ky/distribution/index.js from /Users/arx/try-server-side-ts/dist/handlers/index.js not supported.
Instead change the require of /Users/arx/try-server-side-ts/node_modules/ky/distribution/index.js in /Users/arx/try-server-side-ts/dist/handlers/index.js to a dynamic import() which is available in all CommonJS modules.
at Object.<anonymous> (/Users/arx/try-server-side-ts/dist/handlers/index.js:10:50)
at Object.<anonymous> (/Users/arx/try-server-side-ts/dist/index.js:7:19) {
code: 'ERR_REQUIRE_ESM'
}
Node.js v18.9.0
- .swcrc / module.type を "es6" にすると別のエラーが起きたり、色々試したが今のところ解決できてない。誰か助けて。
なぜ swc にしたのか? (なぜ他の〇〇にしなかったのか?)
Parcel 2 にしなかった理由
- single js になってしまうから。
- minify (dead code eliminate etc.) が効かないっぽく、build size が肥大化するから。
esbuild にしなかった理由
- single js になってしまうから。
-
bundle: false,
の設定をしてもダメ。
-
etsc (esbuild-node-tsc) にしなかった理由
- Absolute path import できなかったから。
vite-plugin-node にしなかった理由
- 開発 build で debugger を attach できなかったから。
まとめ
- 激遅 tsc build や、読みづらい relative path import や、node_modules 丸コピの Fat Docker image などを解決できた。
- 正直 yarn berry にするメリットが感じられない。PnP/Zero-Installs 使わないのであれば、設定や binary 管理が増えるデメリットしかない気がする。
- esbuild の方が勢いある感じもするが、swc も Next.js や Parcel 2 に採用されるなどしてるので頑張ってほしい。
Discussion