【2022Q2】pnpm Workspaceによるmonorepo構成のnpmパッケージ開発環境構築

# 概要

手元の雑多なプライベートnpmパッケージ類をmonorepoに押し込んで効率的に管理しつつ、公開できるものは公開してしまおう、という試みにおける開発環境構築の記録。  
pnpm WorkspaceによるmonorepoでTypeScriptからESM/CJS両対応またはESM専用のパッケージを生成し、Changesetsによるパッケージ別のバージョン管理と、これに連動した一括デプロイを行う、というもの。  

ステップ毎に各設定ファイル等の一部を引用して記述しているため、全体像は完成形の下記ボイラープレートを参照のこと。

https://github.com/macropygia/pnpm-ts-pkg-boilerplate

- 続編: [【2022Q3】pnpm+Changesets+GitHub Actionsでmonorepo内のnpmパッケージのリリースを半自動化する](https://qiita.com/macropygia/items/bd22ec12c2be108b3357)

## 要件

- 作業はWSL2のDebian上で行う
- [Node.js](https://nodejs.org/) は [tj/n](https://github.com/tj/n) で管理しバージョンは16系を使用する
- リポジトリは [pnpm](https://pnpm.io/) Workspaceを使用したmonorepo構成とする
- 各パッケージのバージョンは個別管理とする
- Gitプラットフォームは [GitHub](https://github.com/) を使用する
- 開発言語は [TypeScript](https://www.typescriptlang.org/) とする
- Linterには [ESLint](https://eslint.org/) を、Formatterには [Prettier](https://prettier.io/) を使用し [EditorConfig](https://editorconfig.org/) を併用する
- ドキュメンテーションは [TSDoc](https://tsdoc.org/) に従い、出力が必要な場合は [TypeDoc](https://typedoc.org/) を使用する
- エディタには [Visual Studio Code](https://code.visualstudio.com/) を使用し、必要に応じてコーディング用フォントと拡張機能を導入する
- [Simple Git Hooks](https://github.com/toplenboren/simple-git-hooks) と [lint-staged](https://github.com/okonet/lint-staged#readme) でコミット時にLinter/Formatter/型チェックを自動で実行する
- 英語版と日本語版のREADMEを作成し、 [Shields.io](https://shields.io/) で各種バッジを貼る
- [Changesets](https://github.com/changesets/changesets) でバージョニングとCHANGELOGの生成を行う
- [tsup](https://tsup.egoist.sh/) でESModule/CommonJS両対応のデュアルパッケージとしてビルドする
   - またはTypeScriptでESM専用モジュールとしてビルドする
- pnpmでpackage.json上のバージョンが上がったパッケージをnpmに一括デプロイする

<details><summary>執筆時の詳細なバージョン情報</summary>

| Name             | Version |
| ---------------- | ------- |
| Windows11        | 21H2    |
| Debian           | 11.3    |
| git              | 2.36    |
| Node.js          | 16.15.1 |
| pnpm             | 7.2.0   |
| typescript       | 4.7.3   |
| eslint           | 8.17.0  |
| prettier         | 2.6.2   |
| typedoc          | 0.22.17 |
| simple-git-hooks | 2.8.0   |
| lint-staged      | 12.0.1  |
| tsup             | 6.1.2   |

</details>

# 構築

- 便宜上コードブロックのファイル名はプロジェクトルートをルートディレクトリと見なした絶対パスで記述する
- WSL2の準備は省略
    - 概略：BIOSでCPUの仮想化機能を有効化 → WSLを有効化 → Microsoft Storeから [Debian](https://apps.microsoft.com/store/detail/debian/9MSVKQC78PK6) をインストール
- [Gitのインストール](https://git-scm.com/book/ja/v2/%E4%BD%BF%E3%81%84%E5%A7%8B%E3%82%81%E3%82%8B-Git%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)は省略
    - 概略：上記リンクの「ソースからのインストール」の通り

## 1. Node.jsのインストール

Node.jsのバージョン管理ツール [tj/n](https://github.com/tj/n) を [n-install](https://github.com/mklement0/n-install) を使用してインストールし、Node16系の最新版をインストールする。

```shell:shell
# tj/nのインストール（n-install使用）
$ sudo apt-get update
$ sudo apt-get -y install curl
$ curl -L https://bit.ly/n-install | bash

# 16系の利用可能なバージョンを確認
$ n lsr --all | grep ^16
16.15.1
16.15.0
16.14.2
...

# 16系の最新バージョンをインストール
$ n i 16.15.1
$ node -v
16.15.1
```

## 2. pnpmのインストール

npmで [pnpm](https://pnpm.io/) をグローバルインストールする。

```shell:shell
$ npm i -g pnpm
```

- [pnpm公式ドキュメント](https://pnpm.io/ja/motivation)
    - 日本語化完了率100%だとか（2022-06-05現在）

## 3. リポジトリ作成

プロジェクトのディレクトリを作成し、gitリポジトリとして初期化して設定を行う。

```shell:shell
# リポジトリのディレクトリを作成
$ mkdir project-name
$ cd project-name

# git初期化
$ git init
$ git config core.filemode false
# その他必要に応じてユーザ名・メールアドレス・リモートリポジトリ等を設定
```

- Windows環境で編集される可能性があるリポジトリは `filemode = false` にした方がよさそう
    - パーミッションが重要なリポジトリはWinでは触らないはず
    - 同様に `ignorecase` をどうするかも悩ましい（WinのFSは大文字小文字を区別しない）

### 3-1. .gitignore

`.gitignore` ファイルを作成して設定する。  
[GitHub公式の.gitignoreテンプレート](https://github.com/github/gitignore) から各OS用とNode.js用の内容を抽出して結合したものをベースとする。

```conf:/.gitignore
# tsup
dist

# TypeDoc
packages/*/docs

# VSCode
.vscode
*.code-workspace

# WSL
*Zone.Identifier

# 以下テンプレートの内容
```

## 4. ルートプロジェクト設定

ルートプロジェクトとはmonorepoにおける親となるリポジトリのこと。子はサブプロジェクト。  
ルートプロジェクトの `package.json` とpnpmの設定ファイルを作成し編集する。

### 4-1. 初期化

```shell:shell (root project)
# package.jsonの雛形を生成
$ pnpm init
```

### 4-2. package.json

```json-doc:/package.json
{
  "name": "<project-name>",
  "version": "0.0.1", // 非公開なので重要ではないが、大きな変更があったら適宜手動でインクリメント予定
  "description": "<project-description>",
  "author": "<author-name>",
  "private": true, // ルートプロジェクトは公開しない
  "homepage": "https://github.com/<github-username>/<repository-name>",
  "repository": {
    "type": "git",
    "url": "https://github.com/<github-username>/<repository-name>.git"
  },
  "bugs": {
    "url": "https://github.com/<github-username>/<repository-name>/issues"
  },
  "scripts": {
    "preinstall": "npx -y only-allow pnpm" // pnpmを強制する
  },
  "engines": {
    "node": ">=16.8.0", // 後述
    "npm": "use pnpm", // pnpmを強制する
    "pnpm": ">=7",
    "yarn": "use pnpm" // pnpmを強制する
  }
}

```

- [npmにおけるpackage.json仕様（V8系）](https://docs.npmjs.com/cli/v8/configuring-npm/package-json)
    - `package.json` の内容にはnpm用の記述とNode.js用の記述が同居している点に注意、兼用のフィールドもある
    - 参考: [Node.jsにおけるpackage.jsonフィールド定義](https://nodejs.org/api/packages.html#nodejs-packagejson-field-definitions)
- `engines.node` は [node.green](https://node.green/) などを参考に同じメジャーバージョン内で互換性のありそうな最も古いバージョンを指定した

### 4-3. サブプロジェクト検出パターン設定

`pnpm-workspace.yaml` を作成して設定する。

```yaml:/pnpm-workspace.yaml
packages:
  - 'packages/*'
```

- [公式ドキュメント](https://pnpm.io/ja/pnpm-workspace_yaml)
- 今回はpackagesディレクトリ直下のディレクトリをサブプロジェクトと見なす

## 5. サブプロジェクト設定

サブプロジェクトの `package.json` を作成し編集する。  

### 5-1. 初期化

```shell:shell (root project)
# ディレクトリ追加
$ mkdir packages
$ mkdir packages/package-name
$ cd packages/package-name
```

```shell:shell (sub project)
# 初期化
$ pnpm init
```

### 5-2. package.json

```json-doc:/packages/package-name/package.json
{
  "name": "<package-name>",
  "version": "0.0.0", // Changesetsが自動でインクリメントするため0.0.0にしておく
  "description": "<package-description>",
  "author": "<author-name>",
  "license": "<license>",
  "homepage": "https://github.com/<github-username>/<repository-name>/tree/main/packages/<package-name>#readme", // GitHub上のREADME.mdを使う場合
  "repository": {
    "type": "git",
    "url": "https://github.com/<github-username>/<repository-name>.git",
    "directory": "packages/<package-name>" // package.jsonのあるサブディレクトリを指定
  },
  "bugs": {
    "url": "https://github.com/<github-username>/<repository-name>/issues"
  },
  "keywords": [
    "hoge", // 後述
    "fuga"
  ],
  "files": [ // パッケージに含めるファイル
    "dist" // pnpm publishを再帰的に実行する際は `./dist` だと処理されない
  ]
}
```

- `package.json` などは `files` フィールドで指定しなくても標準でパッケージに含まれる
    - 詳細は [npmにおけるpackage.json仕様（V8系）](https://docs.npmjs.com/cli/v8/configuring-npm/package-json#files) を参照
- 後述するがルートプロジェクトでpnpm Workspaceの機能 `pnpm -r publish` を使用して再帰的にデプロイする場合、 `files` フィールドのパス指定の先頭に `./` があると処理されない
    - 2022Q1に再帰的なpublishがfilesフィールドを参照しないバグが起票されて解決しており、その結果が現状であるため、現時点では仕様と思われる
    - 相対パスを `./` から始める流派の人は要注意
    - サブプロジェクト毎に `npm publish` （pnpmではなくnpmでのデプロイ）を行う場合は `./` が入っていても問題ない
- `keywords` フィールドは [npm](https://npmjs.com) のパッケージ情報Readmeタブ最下部の `Keywords` 欄に表示される
    - 検索対象なので過不足のないように
- npmで公開しないものは `private: true` を指定しておく

## 6. TypeScript設定

[TypeScriptコンパイラ](https://www.npmjs.com/package/typescript)を導入し設定する。

```shell:shell (root project)
# インストール
$ pnpm add -wD typescript @tsconfig/node16-strictest-esm
```

### 6-1. tsconfig.json

- [TSConfig公式リファレンス](https://www.typescriptlang.org/tsconfig)
    - [日本語版](https://www.typescriptlang.org/ja/tsconfig)もそこそこの翻訳率
- `tsconfig.json` は [TypeScriptコンパイラ](https://www.npmjs.com/package/typescript) の設定ファイル、即ち `tsc` コマンド実行時の設定を行うもの
    - 内容は大別すると入出力に関する設定とコーディング規約に関する設定に分かれている
- 入出力に関する設定は当然ながらプロジェクトごとに異なるため、プロジェクトごとに `tsconfig.json` が必要になる
    - 互換コンパイラ（ESBuild等）も使用する
- コーディング規約に関する設定はESLintのrulesに相当する
    - こちらはLinterやFormatterが利用する
- `tsconfig.json` を参照するLinterに「npm-scriptsやテストで使用するスクリプト」や「 `*rc.ts` `*.config.ts` 系の設定ファイル」などもチェックさせたい場合、これらはコンパイルには無関係なファイルなので、コンパイル用の `tsconfig.json` の `include` に含めてしまうのは違和感がある
    - 設定によっては「どこからも参照されていない孤立ファイルがある」として怒られることも
    - コンパイル用の `tsconfig.json` は過不足のないように設定し、Linter用にコンパイル用の設定を継承して調整したものを別途用意するのが妥当と思われる

#### ルートプロジェクト

[@tsconfig/bases](https://github.com/tsconfig/bases) に収録されている `@tsconfig/node16-strictest-esm` を継承し、適宜オーバーライドする。

```json-doc:/tsconfig.json
{
  "extends": "@tsconfig/node16-strictest-esm/tsconfig.json",
  "compilerOptions": {
    "lib": ["esnext"], // enginesで指定したNode.jsが属するバージョンを選択

    // 以下お好みで
    "resolveJsonModule": true,
    "moduleResolution": "node", // resolveJsonModuleをtrueにする場合は継承していても書かないとエラーになる
    "declaration": true,
    "noEmitOnError": true,
    "allowSyntheticDefaultImports": true,
    "checkJs": false
  }
}
```

#### サブプロジェクト

ルートプロジェクトを継承し、入出力を設定する。

```json-doc:/packages/package-name/tsconfig.json
{
  "extends": "../../tsconfig.json", // ルートプロジェクトから継承
  "compilerOptions": {
    "outDir": "./dist" // コンパイル先はdistとする
  },
  "include": ["src/**/*"] // ソースはsrc配下に収める
}
```

### 6-2. 型チェック

サブプロジェクトのnpm-scriptsに型チェックの設定を追加する。

```json-doc:/packages/package-name/package.json
"type-check": "tsc --noEmit -p ."
```

- [tsc CLIオプション（公式）](https://www.typescriptlang.org/docs/handbook/compiler-options.html)
- コミット時の自動チェックに使用する
- なお、サブプロジェクトで共通のビルド・テスト関連の依存関係はホイストを利用してルートプロジェクトの `devDependencies` で賄うこととする
    - `typescript` `jest` `ts-jest` `tsup` `rimraf` など
    - それ以外の型定義などは個々のサブプロジェクトの `devDependencies` に入れる

## 7. ドキュメンテーション設定

[TypeDoc](https://typedoc.org/) を導入し [TSDoc](https://tsdoc.org/) 形式のドキュメンテーションをドキュメントとして出力できるようにする。

```shell:shell (root project)
# インストール
$ pnpm add -wD typedoc
```

- TSDocはTypeScriptの開発元であるMicrosoft謹製のドキュメンテーション仕様
    - 概ねJSDocから型関係の記述を省略したもの
    - 執筆時のバージョンが `0.14.1` であることからわかるように、まだ正式版ではない
- 今のところ [@microsoft/tsdoc](https://www.npmjs.com/package/@microsoft/tsdoc) には出力機能がないため [TypeDoc](https://typedoc.org/) を使用する

```json-doc:/packages/package-name/package.json
"typedoc": "typedoc src/index.ts"
```

- サブプロジェクトのnpm-scriptsにTypeDoc出力の設定を追加する

```plaintext:/.gitignore
packages/*/docs
```

- TypeDocは標準設定では `docs` ディレクトリに出力するのでgitに無視させる

## 8. Linter/Formatter設定

[ESLint](https://eslint.org/) と [Prettier](https://prettier.io/) および各種設定を導入し、文法チェックとフォーマットを設定する。

```shell:shell (root project)
# インストール
$ pnpm add -wD eslint eslint-define-config eslint-plugin-n \
  @typescript-eslint/eslint-plugin @typescript-eslint/parser \
  eslint-plugin-import eslint-import-resolver-typescript \
  eslint-plugin-tsdoc　eslint-config-prettier prettier
```

- `eslint-define-config` は複雑になりがちな `.eslintrc.js` をTSの型定義を利用して文法チェックする仕組み
- `eslint-plugin-n` は更新が途絶えていたNode.js用プラグインの後継

### 8-1. ESLint

```js:/eslintrc.cjs
// @ts-check
// ↑ eslint-define-config用
const { defineConfig } = require('eslint-define-config') // eslint-disable-line

module.exports = defineConfig({
  root: true,
  env: {
    es2021: true,
  },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended', // TypeScript用の設定はこれで全て入る
    'plugin:import/recommended', // eslint-plugin-import
    'plugin:n/recommended', // eslint-plugin-n
    'prettier',
  ],
  plugins: ['eslint-plugin-tsdoc'],
  parserOptions: {
    ecmaVersion: 'latest',
  },
  settings: {
    'import/parsers': { // import用設定
      '@typescript-eslint/parser': ['.ts'],
    },
    'import/resolver': { // import用設定
      typescript: {
        alwaysTryTypes: true,
        project: 'packages/*/tsconfig.json',
      },
    },
  },
  rules: {
    // 必須設定
    // eslint-plugin-node
    'n/no-missing-import': 'off', // 拡張子を省略したimportがエラーになるため無効化
    'n/no-unsupported-features/es-syntax': 'off', // importを使うので無効化
    // eslint-plugin-import
    'import/order': ['error', { 'newlines-between': 'always' }], // エラーになるよう設定することで自動修正が効くようになる
    'import/no-named-as-default-member': 'off', // デュアルパッケージでビルドする場合、この設定に従うと一部のimportが動作しない
    // eslint-import-resolver-typescript
    'import/no-unresolved': 'error', // 解決できないimportをエラー扱いにする
    // eslint-plugin-tsdoc
    'tsdoc/syntax': 'warn', // TSDoc関連は全てwarning扱いにする

    // 以下お好みで
    '@typescript-eslint/no-non-null-assertion': 'off', // Mapが使いづらいため
    '@typescript-eslint/no-implicit-any-catch': [ // try..catchでerrorを扱うため
      'error',
      { allowExplicitAny: true },
    ],
    '@typescript-eslint/no-explicit-any': 'off', // 厳しすぎる
    '@typescript-eslint/no-empty-interface': 'off', // 厳しすぎる
  },
})
```

- [ESLint設定ファイルに関する公式ドキュメント](https://eslint.org/docs/user-guide/configuring/configuration-files#configuration-file-formats)
- `extends` はその名の通り全体の様々な箇所に設定を注入するため、適宜定義プロジェクト側でオーバーライドする必要がある
- 各extendsの内容を知りたければそれぞれの設定ファイルの原本を参照する
    - [@typescript-eslint/recommended](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/src/configs/recommended.ts)
        - `@typescript-eslint/base` と `@typescript-eslint/eslint-recommended` を含んでいることがわかる
    - [plugin:import/recommended](https://github.com/import-js/eslint-plugin-import/blob/main/config/recommended.js)
    - [plugin:n/recommended](https://github.com/weiran-zsd/eslint-plugin-node/blob/master/lib/configs/recommended.js)

```plaintext:/.eslintignore
packages/*/dist
packages/*/docs
packages/*/coverage
packages/*/__tests__/src
packages/*/__tests__/dist
jest.config.cjs
!.*.js
!.*.cjs
!.*.mjs
!.*.ts
```

- [.eslintignoreに関する公式ドキュメント](https://eslint.org/docs/user-guide/configuring/ignoring-code#the-eslintignore-file)
- 以下を無視する
    - 各種出力ファイル（まだ説明していないものは後述する）
    - 敢えて瑕疵のあるソースを含める可能性があるテスト用ファイル類（後述）
    - TSDocに怒られる `jset.config.cjs` （回避手段検討中）
- dotファイル類は逆に標準ではチェックされないため「無視リストから外す」記述を行う必要がある
    - dotディレクトリについても同様
    - 公式ドキュメントに動作機序とサンプルが載っている

```json-doc:/package.json
"lint": "eslint --fix **/*.{ts,d.ts,js,cjs,mjs}"
```

- ルートプロジェクトのnpm-scriptsに追加
- `.eslintignore` が正しく設定できていれば対象拡張子の全ファイルを対象に実行すればよい

### 8-2. Prettier

```json-doc:/.prettierrc
// お好みで
{
  "singleQuote": true,
  "semi": false
}
```

- [公式ドキュメント](https://prettier.io/docs/en/configuration.html)
- これがトレンドのようなので

```plaintext:/.prettierignore
packages/*/dist
packages/*/docs
packages/*/coverage
packages/*/__tests__/src
packages/*/__tests__/dist
*.md
```

- [公式ドキュメント](https://prettier.io/docs/en/ignore.html)
- 概ね `.eslintignore` と同様
- Markdownのフォーマットはなんか変なので使用しない
    - VSCodeの `markdownlint` に任せる
        - `markdownlint-cli` か `markdownlint-cli2` を入れて自動化してもいいがひとまずスキップ

```json-doc:/package.json
"prettier": "prettier --write --ignore-unknown **/*"
```

- ルートプロジェクトのnpm-scriptsに追加
- `.prettierignore` が正しく設定できていれば全ファイルを対象に実行すればよい

### 8-3. EdiotorConfig

```ini:/.editorconfig
# お好みで
root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
indent_size = 4
trim_trailing_whitespace = false

[*.json]
insert_final_newline = false
```

## 9. VSCode

- WSLで開発するなら必須感がある
- そろそろ `files.excludeGitIgnore` が利用できるようになりそう（何年越し？）

### 9-1. 拡張機能

- [Remote - WSL](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl)
    - VSCodeから直接WSL内のソースを扱う、ターミナルもWSLのものになる
- [EditorConfig for VS Code](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig)
    - `.editorconfig` を反映する
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
    - ESLintのエラーを表示する
- [Prettier - Code formatter](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
    - Prettierでフォーマットする
- [markdownlint](https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint)
    - PrettierのMarkdownフォーマッタがなんか変なのでこちらで代替する
    - テーブル整形用に [Markdown Table](https://marketplace.visualstudio.com/items?itemName=TakumiI.markdowntable) を組み合わせる
- その他便利系
    - [GitLens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens)
    - [zenkaku](https://marketplace.visualstudio.com/items?itemName=mosapride.zenkaku) ※コーディング用フォントを使用する場合はなくてもよい
    - [Quick and Simple Text Selection](https://marketplace.visualstudio.com/items?itemName=dbankier.vscode-quick-select)
    - [Trailing Spaces](https://marketplace.visualstudio.com/items?itemName=shardulm94.trailing-spaces)
    - [Path Autocomplete](https://marketplace.visualstudio.com/items?itemName=ionutvmi.path-autocomplete) ※引用符外でも補完を有効にするオプションがある
    - [Insert Numbers](https://marketplace.visualstudio.com/items?itemName=Asuka.insertnumbers)
    - [Incrementor](https://marketplace.visualstudio.com/items?itemName=nmsmith89.incrementor)
    - [indent-rainbow](https://marketplace.visualstudio.com/items?itemName=oderwat.indent-rainbow)

### 9-2. フォント

- お好みで
- 個人的には [PlemolJP](https://github.com/yuru7/PlemolJP) を使用

## 10. precommitフック

[Simple Git Hooks](https://github.com/toplenboren/simple-git-hooks) と [lint-staged](https://github.com/okonet/lint-staged#readme) を導入し、コミット時に自動で文法チェック・フォーマット・型チェックが行われるように設定する。

```shell:shell (root project)
# インストール
$ pnpm add -wD simple-git-hooks lint-staged
```

```json-doc:/package.json
{
  // ...
  "scripts": {
    "prepare": "simple-git-hooks"
  },
  "simple-git-hooks": {
    "pre-commit": "pnpm exec lint-staged --concurrent false -c lint-staged.mjs"
  },
  // ...
}
```

- 型チェックをステージされたパッケージに絞って実行するためlint-stagedの設定は外に出してJavaScriptで記述する
- lint-stagedの設定ファイルの外部ファイル化は `package.json` を長くしたくない場合も有効

```js:/lint-staged.mjs
export default {
  // Prettier
  '**/*': 'prettier --write --ignore-unknown',
  // ESLint
  '**/*.{ts,d.ts,js,cjs,mjs}': 'eslint --fix',
  // TypeScript
  'packages/*/src/**/*.ts': (filenames) => {
    const updatedPackages = new Set()
    const reGetPackageName = /packages\/(?<packageName>.+)\/src/
    for (const filename of filenames) {
      const found = filename.match(reGetPackageName)
      updatedPackages.add(found.groups.packageName)
    }
    return `pnpm --filter ${[...updatedPackages].join(' --filter ')} type-check`
  },
}
```

- PrettierとESLintの設定はnpm-scriptsと同一
- 各パッケージのsrcディレクトリ内のTSファイルがステージされていた場合、ステージされたTSファイルを含むパッケージのみ型チェックを実行する
    - pnpmの `--filter` オプションでパッケージを指定し、各パッケージの `npm-scripts` にある `type-check` を実行している
        - `--filter` オプションは複数指定して並列実行できる

```shell:shell (root project)
$ pnpm prepare
```

- 忘れずにフックを有効化する

## 11. ビルド

### 11-1. デュアルパッケージ

[tsup](https://tsup.egoist.sh/) を導入してESModuleとCommonJS対応のデュアルパッケージでビルドし、サブプロジェクトの `package.json` をデュアルパッケージに対応させる。

```shell:shell (root project)
# インストール
$ pnpm add -wD tsup rimraf
```

- [tsup公式サイト](https://tsup.egoist.sh/)
- 依存関係のimportは自動で除外するが、うまく動かない場合は `tsup-node` コマンドを使用して欲しいとのこと
- 競合としては [unbuild](https://github.com/unjs/unbuild#readme) がある

```json-doc:/packages/package-name/package.json
{
  // ...
  "main": "./dist/index.js",
  "module": "./dist/index.mjs", // どこで定義されているか不明
  "types": "./dist/index.d.ts", // TypeScriptによって定義されているフィールド
  "exports": { // Node.js 12 で追加された、パターンへの対応は14から
    ".": {
      "require": "./dist/index.js",
      "import": "./dist/index.mjs",
      "types": "./dist/index.d.ts"
    }
  },
  "scripts": {
    "clean": "rimraf ./dist", // 出力ディレクトリを削除
    "tsup": "tsup src/index.ts --dts --format esm,cjs", // 型定義を出力、ESModuleとCommonJSを出力
    "build": "pnpm clean && pnpm tsup" // 上二つを順次実行
  },
  // ...
}
```

- [Node.jsにおけるpackage.jsonフィールド定義](https://nodejs.org/api/packages.html#nodejs-packagejson-field-definitions)
    - `package.json` の内容にはnpm用の記述とNode.js用の記述が同居している点に注意、兼用のフィールドもある
        - 例えば `name` や `main` フィールドは兼用、 `files` フィールドはnpm用
    - 参考: [npmにおけるpackage.json仕様（V8系）](https://docs.npmjs.com/cli/v8/configuring-npm/package-json)

### 11-2. ESM専用パッケージ

TypeScriptのESM対応により今後はESM専用のパッケージも増えていくと思われる。依存関係にESM専用パッケージを含むパッケージは自身もESM専用にならざるを得ない。
TSCでESM専用パッケージをビルドする場合は以下のような設定になると思われる。（2022-07-10時点）

```json-doc:/packages/package-name/package.json
{
  // ...
  "type": "module",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "scripts": {
    "clean": "rimraf ./dist", // 出力ディレクトリを削除
    "tsc": "tsc -p .", // tsconfig.jsonの設定に従ってコンパイル
    "build": "pnpm clean && pnpm tsc" // 上二つを順次実行
  },
  // ...
}
```

```json-doc:/packages/package-name/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "module": "esnext",
    "removeComments": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*"]
}
```


## 12. README

- `README.md` はnpmパッケージとして公開した際にパッケージのホームとして表示されるため英語で記述する
- 日本語のREADMEは `README.ja.md` や `README.ja_JP.md` としておき、冒頭でリンクを貼ることとする
- 有名所のパッケージ等を参考に最初に規格を統一しておいた方が後々面倒がない
    - とはいえ凝ろうと思えばどこまでも凝ることができるため時間を溶かさない程度で

### 12-1. バッジ

- [Shields.io](https://shields.io/)
    - 様々なサービスのエンドポイントからSVGのバッジを生成するサービス
        - npmパッケージであれば公開中のバージョンや設定しているライセンス、qualityやmaintenance等のレートなどが表示できる
        - 若干わかりづらいが、サイトを開いた際にフォーカスの当たっている水平線は検索用の入力欄であり、名称やサービスのURLから対応しているエンドポイントを検索できる
    - テキストとアイコン（次項参照）を組み合わせた自由記述での生成も可能
- [Simple Icons](https://simpleicons.org/)
    - Shields.ioで使用できるサービス・技術・ファイルタイプ・ブランド・企業などのアイコン
    - [全アイコンslugリスト](https://github.com/simple-icons/simple-icons/blob/develop/slugs.md)

例としてパッケージとしてのnpmのバージョンとライセンスを表示する場合は以下のようになる。

```md:markdown
![npm version](https://img.shields.io/npm/v/npm.svg)
![License](https://img.shields.io/npm/l/npm)

<!-- リンクを貼るなら更に囲む -->
[![npm version](https://img.shields.io/npm/v/npm.svg)](https://www.npmjs.com/package/npm)
```

![npm version](https://img.shields.io/npm/v/npm.svg) ![MIT](https://img.shields.io/npm/l/npm)


## 13. Changesets

Changesetsを導入し、対話型CLIで `CHANGELOG.md` の追記と `package.json` のバージョンのインクリメントを行う。  

```shell:shell (root project)
# インストール
$ pnpm add -wD @changesets/cli

# 初期化
$ pnpm changeset init
```

- Changesetは標準でpnpmのworkspaceに対応しておりインストールするだけで設定不要で使用できる
- pnpmの公式ドキュメントにも[Changesetsとの連携のページ](https://pnpm.io/ja/using-changesets)がある

### 13-1. アップデート対象パッケージの選択とサマリーの入力

```shell:shell (root project)
$ pnpm changeset
```

- Changesetsによるバージョン管理はコミット等には紐付いておらず、完全に任意のタイミングで実行できる
- `changeset` コマンドを実行すると対話式CLIで更新するパッケージの選択と `CHANGELOG.md` のサマリーの入力を行う
- 基本的な流れは以下の通り（選択内容やその時点のバージョンによって変化する）
    1. 更新するパッケージの選択
    2. メジャーバージョンを上げるパッケージの選択  
    3. マイナーバージョンを上げるパッケージの選択  
    4. パッチバージョンを上げるパッケージの選択  
    5. サマリーの入力
    6. 入力確認
- `changeset` コマンドを最後まで進めるとルートプロジェクトの `.changeset` ディレクトリにランダムな名前のMarkdownファイルが生成される
    - このMarkdownには対象サブプロジェクトとバージョンアップの種別を示すメタデータ、および `CHANGELOG.md` に追記される内容（=入力したサマリー）が記録されている
    - 次節の操作でサブプロジェクトの `CHANGELOG.md` に適切に転記される
- サマリーを複数入力する場合やパッケージ毎に異なるサマリーを入力する場合は `pnpm changeset` を複数回実行する
    - 変更が発生する度に `pnpm changeset` でサマリーを入力しておけば抜け漏れを防げる
    - 生成されたMarkdownを編集しても構わないが、トップレベルの箇条書きの項目を増やしたい場合は `pnpm changeset` を複数回実行するしかない
- 次節で行う操作で書き換わった後の `CHANGELOG.md` を直接編集しても問題ないため、ケースバイケースで使い分けるとよい

### 13-2. CHANGELOG.mdの更新とバージョンのインクリメント

```shell:shell (root project)
$ pnpm changeset version
```

- `.changeset` ディレクトリ内のMarkdownファイルで指定されたメタデータに従って対応するパッケージのpackage.jsonのバージョンをインクリメントする
- `.changeset` ディレクトリ内のMarkdownファイルの内容を各パッケージの `CHANGELOG.md` に追記する
    - 完了すると `.changeset` ディレクトリ内のMarkdownは削除される

## 14. デプロイ

pnpmのpublishコマンドを再帰実行フラグ（ `-r` ）付きで使用し、バージョン情報を元に更新されたパッケージを全てnpmにデプロイする。

```shell:shell (root project)
# 念のため確認したければドライラン
$ pnpm -r publish --dry-run

# ワンタイムパスワードを指定して実行
$ pnpm -r publish --otp <one_time_password>
```

- npmに公開中のバージョンと `package.json` のバージョンが比較され、更新されているもののみデプロイされる
- Changesetsを適切に使用していればデプロイ対象のパッケージは `package.json` のバージョンが上がっているため自動的にデプロイ対象になる
- gitのステージは綺麗な状態にしておく必要がある
    - テスト等の際は `--no-git-checks` で無効化できるが、普段は有効にして安全なデプロイフローを習慣化した方がよさそう
- 当然ながらnpmにログインしている必要がある

## 15. 依存関係の更新

手動で行う場合は定番のCLIツールである [npm-check-updates](https://www.npmjs.com/package/npm-check-updates) が有用。

```shell:shell (root project)
# ncuをサブディレクトリを含めて再帰的に実行する
$ ncu --deep

# ncuの結果をpackage.jsonに反映する
$ ncu -u --deep
```

- グローバルインストールしてもいいし `pnpm dlx` 等でオンデマンドで実行してもよい
- 対話モードやフィルタ機能、更には更新されたパッケージのアップデート後に自動でテストを実行するモードなどもあり、かなり高機能
- 細かい条件がある場合は設定ファイル（ `.ncurc.{json,yml,js}` ）に書き出しておくこともできる

# 関連記事

本稿の13-2～14の手順をGitHub ActionsとChangesets Release Actionで自動化した。

https://qiita.com/macropygia/items/bd22ec12c2be108b3357

# ToDo

- ~~Jest, ts-jest, vitest等でテストを記述しCoverageを取得する~~ → 記事にできるほどの内容にはならなかった
- ~~GitHub Actionsでテスト、ビルド、およびnpmjs.comへのデプロイを自動化する~~ → 関連記事参照
    - Coverageデータを [Codecov](https://about.codecov.io/) に展開する
- [Renovate](https://www.mend.io/free-developer-tools/renovate/) をDockerでセルフホストし、GitHub Actionsで自動的にPRを発行する