npm package を作る度にイチから開発環境の構築をしていて大変だったので、自分専用のテンプレートリポジトリを作りました *1。
せっかくなので、テンプレートの特徴とか、どういうこと考えながら作ったとか紹介してみます。
はじめに: 基本的な技術スタック
- npm
- TypeScript
- Node.js Native ESM
- Prettier
- ESLint
- Vitest
- Renovate
- GitHub Actions
- vscode 向けの各種設定ファイル (
extensions.json
,launch.json
,settings.json
)
GitHub の「テンプレートリポジトリ」機能を使う
GitHub にそれっぽい機能があったので使ってみました。
「Use this template」というボタンが出て便利です。
yarn/pnpm ではなく npm を使う
以前は yarn や pnpm も使ってましたが、npm だけでも十分だったので npm にしました。yarn/pnpm にあった機能は、以下のような代替機能に移行すれば良いかなと思ってます。
- yarn/pnpm workspace => npm workspace
yarn patch
/pnpm patch
=> https://www.npmjs.com/package/patch-packageyarn upgrade-interactive
/pnpm update -i
=> https://www.npmjs.com/package/npm-checkyarn why
/pnpm why
=>npm why
npm workspace を使っている人そこまで居ないはずなので、安定性が気になってましたが、happy-css-modules で試している限りはちゃんと動いてそうでした。細かいバグを踏み抜く可能性はあると思いますが、一般的な使い方をしている限りはそう困らないんじゃないかなと。
tsconfig.json
は tsconfig/bases を使って書く
tsconfig.json の設定値は色々あって書くのかなり難しくて、id:mizdra も苦労してました。しかし最近は tsconfig/bases というコミュニティ管理の共有 config 集があるようで、良い感じだったのでこれに乗っかっることにしてみました。
いくつか config があるのですが、npm-package-template では @tsconfig/strictest
と @tsconfig/node18
の 2 つの config を使ってます。
// tsconfig.json { "extends": ["@tsconfig/strictest/tsconfig.json", "@tsconfig/node18/tsconfig.json"], "exclude": ["node_modules", "dist", "bin"], "compilerOptions": { "module": "Node16", "moduleResolution": "node16", "noEmit": true, /* Lint Options */ "noUnusedLocals": false, // Delegate to @typescript-eslint/no-unused-vars in eslint "noUnusedParameters": false, // Delegate to @typescript-eslint/no-unused-vars in eslint /* Debug Options */ "locale": "ja" } }
設定値もかなり妥当なものになっていて、ほぼそのまま受け入れれば十分でした。noUnusedLocals
や noUnusedParameters
など lint 系の設定が ON になっていたので、id:mizdra はそれだけ OFF にしました (未使用変数の警告は eslint で検査するほうが細かい調整ができるので)。
コミュニティ標準に乗っかって楽ができて良いと思います。
*.ts
だけでなく *.js
も tsc
で型検査する
コードを *.ts
で書いて tsc
で型検査するのは当たり前ですが、npm-package-template では更に一歩踏み込んで、*.js
の型検査もやってます。tsc
には JSDoc の型アノテーションのコメントを書いて型付けされた *.js
を型検査する機能があって、それを使ってます (@tsconfig/strictest
を extends すると自動で ON になります)。
*.ts
の型アノテーションと違って、*.js
の JSDoc の型アノテーションは書き方が独特です。TypeScript に慣れ親しんでいる人でも、書き方を調べながら書かないといけないくらいには、書き方が違います (id:mizdra も上記の id:qnighy さんの記事を見ながら書いているくらいなので...)。
難しいですが、id:mizdra としては型があったほうが嬉しいですし、書きまくっていればそのうち書き慣れるはずなので、まあ良いかなということで *.js
の型検査をするようにしてみました。
自分用のテンプレートリポジトリだからこういう思い切った判断をしてますが、複数人で開発するようなリポジトリでこれを入れるのはガッツがないと難しいと思います。
tsconfig.json
の include
オプションは省略する
tsconfig.json
の include
オプションを使うと、そのオプションで指定されたパターンにマッチするファイルと、そこから辿れるファイルが tsc
の型検査の対象となります。
{ "include": ["src/*"], "compilerOptions": { "lib": ["es2019"] // ... } }
つまり以下のようなディレクトリ構成があったとき...
my-app/ ├─ asset/ │ ├─ locale.ts ├─ src/ │ ├─ util/ │ │ ├─ math.ts │ │ ├─ fs.ts (実装途中のモジュールで、まだどこからも依存されていないという設定) │ ├─ index.ts (src/util/math.ts, asset/locale.ts に依存) ├─ package.json ├─ .eslintrc.js ├─ tsconfig.json (`"include": ["src/*"]` が指定されてる)
以下のファイルが tsc
の型検査の対象になります。
src/index.ts
("include": ["src/*"]
にマッチするため)src/util/math.ts
(src/index.ts
から辿れるため)asset/locale.ts
(src/index.ts
から辿れるため)
一方、以下のファイルは型検査の対象になりません。
src/util/fs.ts
.eslintrc.js
そのため、src/util/fs.ts
などに型エラーがあっても CI をすり抜けてしまいます。tsserver (vscode に組み込まれている TypeScript の Language Server) も src/util/fs.ts
をどの tsconfig.json
を使って型検査すればよいか分からないので、エディタ上で実行される型検査もおかしなことになります。
"include: ["src/*"]
と書く人は稀だと思いますが、(TypeScript 公式ドキュメントの include オプションの説明 にも書いてある) "include: ["src/**/*"]
を書いている人は結構居るかもしれません。"include: ["src/**/*"]
であれば src/util/fs.ts
も型検査されるので、ほとんどの人はそれで支障ないと思います。ただ、"include: ["src/**/*"]
でも .eslintrc.js
は型検査されません。僕としてはありとあらゆるファイルを型検査して欲しいので、これでは困ります。
そこで npm-package-template では、include
オプションを省略することにしました。省略すると "include: ["**/*"]
相当の挙動になり、.eslintrc.js
も型検査対象に含まれるようになります。
{ // 省略する // "include": ["src/*"], "compilerOptions": { "lib": ["es2019"] // ... } }
余談ですが、モノレポで、npm workspace などを利用する場合、個々の workspace ごとに型検査に使用する tsconfig.json
を変えたいことがあります (workspace ごとに lib
を変えて型検査したいなど)。そういう場合は exclude
オプションを併用したり、workspace ごとの tsconfig.json
も併置する必要があります。この話をすると長くなるので、詳しくはまた別の機会に。
ビルドは tsc
で行う
ビルドツールには tsc
を使ってます。一般的な Web フロントエンドアプリケーションでは Webpack や Vite など、いわゆる bundler を使ってビルドしますが、別に npm package では bundle する理由はないので、bundler は使いません。*.ts
=> *.js
のトランスパイルは tsc
でできるので、それで十分です。
ビルド用の tsconfig.json
は型検査用のものと別に用意する
ビルド時は noEmit
を外したり、追加でいくつかのオプションを微調整したいことがあるので、型検査用のものとは別の tsconfig.json
を用意してます。
シンプルに型検査用の tsconfig.json
を extends しつつ、ビルド用向けにカスタマイズしているだけです。
{ "extends": "./tsconfig.json", "include": ["src/**/*"], "compilerOptions": { "noEmit": false, "outDir": "./dist", "rootDir": "src", // To avoid inadvertently changing the directory structure under dist/. "sourceMap": true, "declaration": true, "declarationMap": true } }
declarationMap
はあまり知られてないオプションですが、めっちゃ便利なので付けておくと良いです。
rootDir
も dist
ディレクトリ内の構造がうっかり変わって package が壊れないよう、保険として付けておくと良いです *2。
Node.js Native ESM で書く
もう 2023 年だし全部 Node.js Native ESM で書けば良いでしょう、ということでコードは Node.js Native ESM で書くようにしました。本当は Dual Packages (CommonJS と ES Modules) に対応するのが望ましいと思いますが、そのための設定の手間や、Dual Package Hazard の回避のために、一旦 Pure ESM を前提としたテンプレートにしてます。やる気が出たら Dual Packages 対応するかも...。
関連:
yosuke-furukawa.hatenablog.com
Prettier/ESLint/Renovate の設定は shareable config 化したものを使う
自分専用 shareable config を育ててるので、それを使ってます。
- https://github.com/mizdra/prettier-config-mizdra
- https://github.com/mizdra/eslint-config-mizdra
- https://github.com/mizdra/renovate-config-mizdra
mizdra-style npm-scripts に従う
以前以下の記事で紹介した npm-scripts の書き方を npm-package-template でも採用してます。どんなプロジェクトでもこの書き方通りにやれば上手くいくので気に入ってます。
{ "scripts": { "build": "tsc -p tsconfig.build.json", "dev": "npm run build && node bin/example-command.js", "lint": "run-s -c lint:*", "lint:tsc": "tsc", "lint:eslint": "eslint .", "lint:prettier": "prettier --check .", "test": "vitest" }, // ... }
.xxxignore
に書かれているファイル以外全て lint/format する
eslint .
や prettier --check .
で lint/format します。eslint src/**
や prettier --check src/**
とは書きません。何故なら lint 漏れが発生する可能性があるためです。加えて eslint や prettier の vscode 拡張は、原則として .eslintignore
や .prettierignore
に書かれているファイル以外全て lint/format します。そのため、たとえ npm-scripts に eslint src/**
と書いていても、それを無視して src/
ディレクトリ以外のファイルも lint/format してしまいます。
vscode の拡張機能とも lint/format 対象のファイルに違いが出てしまうので、eslint src/**
や prettier --check src/**
と書くのは避けるのが良いです。
参考: ESLint, Prettier, VS Code, npm scripts の設定: 2021春
検査内容ごとに GitHub Actions の job を分ける
1 つの job で lint => test => build と順番にまとめてやると、どれか1つがコケた時点で、後続の検査が実行されなくなってしまいます。例えば lint でコケると、test や build が実行されません。仮に test や build がコケる状態あっても、その時点ではそれが発見されず、lint が通るよう直したあとに気づくという... あるあるだと思います。
これでは困るということで、npm-package-template では 検査内容ごとに job を分けることにしました。
「そんな調子で job の数を増やしていったら、GitHub Actions の同時並列実行数の上限に当たって困るんじゃない?」という意見もあるかもしれませんが、まあそうなったらその時考えれば良いかなと。そもそも今回は lint/test/build の 3 つしか job ないので大した量じゃないはず。
vscode 向けの設定ファイルを用意
id:mizdra は普段 vscode を使ってコーディングしているので、そのコーディングが快適にできるよう、いくつか設定ファイルを用意しました。
- .vscode/extensions.json
- 拡張機能の推奨リスト
- .vscode/settings.json
- formatOnSave の設定や vitest の vscode 拡張の設定などを書いてる
- .vscode/launch.json
- vscode のデバッグ機能を使えるようにするための設定
最後の .vscode/launch.json
がイチオシで、これによって breakpoint を仕掛けながら npm package をデバッグできます。
たった 13 行の設定ファイルを用意するだけで動きます。手間の割に得られるものが大きくてオススメです。
// .vscode/launch.json { "version": "0.2.0", "configurations": [ { "name": "dev", "type": "node", "request": "launch", "cwd": "${workspaceFolder}", "runtimeExecutable": "npm", "runtimeArgs": ["run", "dev"] } ] }
テストランナーには Vitest を使う
今までずっと Jest を使ってきましたが、Node.js Native ESM のコードを Jest でテストしようと思うと、すごく複雑なセットアップが必要で、苦労してました。最近 Vitest を使ってみたところ、ほぼゼロコンフィグで Node.js Native ESM のコードのテストができて感動したので、Vitest に移行することにしました。
「Vitest for VSCode」という vscode 上からテストを実行する拡張機能もあって、新興ライブラリながらエコシステムも整いつつあるように見えてます。まあでもよく触ってみると vscode-jest にはあるけど Vitest for VSCode にはない機能の存在に気づいたり。適時 Issue を立てたりとフィードバックしながら使うことになりそうです。
テストファイルはテスト対象ファイルの横に併置する (コロケーション)
src/math.ts
のテストファイルを src/math.test.ts
に置くという話です。要は以下の実践です。
package.json
の files
field を設定する
package 化したときに含まれるファイルを package.json
の files
field で指定してます。
{ // ... "files": [ "bin", "src", "!src/**/*.test.ts", "!src/**/__snapshots__", "dist" ] }
bin
と dist
だけあれば良いように思うかもしれませんが、*.ts
を package 内に保持しておかないと tsconfig.json
の declarationMap
が機能しないので、src
も必要です。ただ、src
を丸ごと指定すると、コロケーションしているテストファイルも混じってしまうので、!src/**/*.test.ts
と !src/**/__snapshots__
も併記してます。
LICENSE や README も含める必要がありますが、それらは npm が自動で含めてくれるので、省略してます。
リリース方法をドキュメント化する
CONTRIBUTING.md
に npm publish
コマンドなどを使ったリリース方法を書き留めることにしました。
npm publish
のやり方がうろ覚えで、毎回調べていたので...。リポジトリに書いてしまえば間違いようがないはず。コードブロックにすることでコマンドのコピーボタンが表示されるようにしてるのがポイントです。
.github/release.yml
を用意する
GitHub のリリースノート自動生成機能を使うために必要なファイルです。
npm-package-template では id:r7kamura さんの方法を真似て、Keep a Changelog の形式のリリースノートが生成できるよう設定してます。
あとがき
現代的なテンプレートリポジトリを作ることができて満足してます。あとコードを書きまくって得た知見をこうしてテンプレートリポジトリに集約できたのも良かったです。
テンプレートリポジトリ自体は CC0-1.0 で配布してるので、ご自由にお使いください。