mizdra's blog

ぽよぐらみんぐ

npm package を実装するための自分専用テンプレートリポジトリを作った

npm package を作る度にイチから開発環境の構築をしていて大変だったので、自分専用のテンプレートリポジトリを作りました *1

github.com

せっかくなので、テンプレートの特徴とか、どういうこと考えながら作ったとか紹介してみます。

はじめに: 基本的な技術スタック

  • npm
  • TypeScript
  • Node.js Native ESM
  • Prettier
  • ESLint
  • Vitest
  • Renovate
  • GitHub Actions
  • vscode 向けの各種設定ファイル (extensions.json, launch.json, settings.json)

GitHub の「テンプレートリポジトリ」機能を使う

GitHub にそれっぽい機能があったので使ってみました。

docs.github.com

「Use this template」というボタンが出て便利です。

「Use this template」ボタンから、テンプレートを利用したリポジトリを作成できる。

yarn/pnpm ではなく npm を使う

以前は yarn や pnpm も使ってましたが、npm だけでも十分だったので npm にしました。yarn/pnpm にあった機能は、以下のような代替機能に移行すれば良いかなと思ってます。

npm workspace を使っている人そこまで居ないはずなので、安定性が気になってましたが、happy-css-modules で試している限りはちゃんと動いてそうでした。細かいバグを踏み抜く可能性はあると思いますが、一般的な使い方をしている限りはそう困らないんじゃないかなと。

tsconfig.json は tsconfig/bases を使って書く

tsconfig.json の設定値は色々あって書くのかなり難しくて、id:mizdra も苦労してました。しかし最近は tsconfig/bases というコミュニティ管理の共有 config 集があるようで、良い感じだったのでこれに乗っかっることにしてみました。

github.com

いくつか 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"
  }
}

設定値もかなり妥当なものになっていて、ほぼそのまま受け入れれば十分でした。noUnusedLocalsnoUnusedParameters など lint 系の設定が ON になっていたので、id:mizdra はそれだけ OFF にしました (未使用変数の警告は eslint で検査するほうが細かい調整ができるので)。

コミュニティ標準に乗っかって楽ができて良いと思います。

*.ts だけでなく *.jstsc で型検査する

コードを *.ts で書いて tsc で型検査するのは当たり前ですが、npm-package-template では更に一歩踏み込んで、*.js の型検査もやってます。tsc には JSDoc の型アノテーションのコメントを書いて型付けされた *.js を型検査する機能があって、それを使ってます (@tsconfig/strictest を extends すると自動で ON になります)。

zenn.dev

*.ts の型アノテーションと違って、*.js の JSDoc の型アノテーションは書き方が独特です。TypeScript に慣れ親しんでいる人でも、書き方を調べながら書かないといけないくらいには、書き方が違います (id:mizdra も上記の id:qnighy さんの記事を見ながら書いているくらいなので...)。

難しいですが、id:mizdra としては型があったほうが嬉しいですし、書きまくっていればそのうち書き慣れるはずなので、まあ良いかなということで *.js の型検査をするようにしてみました。

自分用のテンプレートリポジトリだからこういう思い切った判断をしてますが、複数人で開発するようなリポジトリでこれを入れるのはガッツがないと難しいと思います。

tsconfig.jsoninclude オプションは省略する

tsconfig.jsoninclude オプションを使うと、そのオプションで指定されたパターンにマッチするファイルと、そこから辿れるファイルが 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 はあまり知られてないオプションですが、めっちゃ便利なので付けておくと良いです。

rootDirdist ディレクトリ内の構造がうっかり変わって 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 対応するかも...。

関連:

quramy.medium.com

yosuke-furukawa.hatenablog.com

Prettier/ESLint/Renovate の設定は shareable config 化したものを使う

自分専用 shareable config を育ててるので、それを使ってます。

mizdra-style npm-scripts に従う

以前以下の記事で紹介した npm-scripts の書き方を npm-package-template でも採用してます。どんなプロジェクトでもこの書き方通りにやれば上手くいくので気に入ってます。

www.mizdra.net

{
  "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/launch.json がイチオシで、これによって breakpoint を仕掛けながら npm package をデバッグできます。

breakpoint を仕掛けながらデバッグする様子。

たった 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 に置くという話です。要は以下の実践です。

www.mizdra.net

package.jsonfiles field を設定する

package 化したときに含まれるファイルを package.jsonfiles field で指定してます。

{
  // ...
  "files": [
    "bin",
    "src",
    "!src/**/*.test.ts",
    "!src/**/__snapshots__",
    "dist"
  ]
}

bindist だけあれば良いように思うかもしれませんが、*.ts を package 内に保持しておかないと tsconfig.jsondeclarationMap が機能しないので、src も必要です。ただ、src を丸ごと指定すると、コロケーションしているテストファイルも混じってしまうので、!src/**/*.test.ts!src/**/__snapshots__ も併記してます。

LICENSE や README も含める必要がありますが、それらは npm が自動で含めてくれるので、省略してます。

リリース方法をドキュメント化する

CONTRIBUTING.mdnpm publish コマンドなどを使ったリリース方法を書き留めることにしました。

npm publish のやり方がうろ覚えで、毎回調べていたので...。リポジトリに書いてしまえば間違いようがないはず。コードブロックにすることでコマンドのコピーボタンが表示されるようにしてるのがポイントです。

コードブロックにすることでコピーボタンが出現する。

.github/release.yml を用意する

GitHub のリリースノート自動生成機能を使うために必要なファイルです。

www.mizdra.net

npm-package-template では id:r7kamura さんの方法を真似て、Keep a Changelog の形式のリリースノートが生成できるよう設定してます。

r7kamura.com

あとがき

現代的なテンプレートリポジトリを作ることができて満足してます。あとコードを書きまくって得た知見をこうしてテンプレートリポジトリに集約できたのも良かったです。

テンプレートリポジトリ自体は CC0-1.0 で配布してるので、ご自由にお使いください。

*1:厳密にはリポジトリ自体は 4 年前からあったのだけど、ちゃんと整備してなくて滅んでいたので整備し直した、という背景。

*2:実際にこのオプションを付けてなかったために、とある package で dist ディレクトリ内の構造を変えて壊してしまうという出来事があった。

ポケットモンスター・ポケモン・Pokémon・は任天堂・クリーチャーズ・ゲームフリークの登録商標です.

当ブログは @mizdra 個人により運営されており, 株式会社ポケモン及びその関連会社とは一切関係ありません.