lerna + yarn workspaces で GitHub Packages の private repository を利用してみた（殴り書きメモ）

「サーバーとクライアントの共有部分をライブラリ化したい → でも非公開にしたい」という要望への解決方法の一つ。コードが JS/TS 一本で、リポジトリと所有権が一カ所で良ければ使いやすそう。

そもそも、公開パッケージとして分離できるならそれでいい話だし。

今回は `Windows`, `nodist`, `node v14`, `yarn install v1.22.10` で試した。

以下では、`github` のユーザ名を `GITHUB_USERNAME`、`github` のリポジトリ名を `GITHUB_REPONAME` と記すことにする（コマンドをコピペして確認していないので細部に誤りがあると思います）。`PACKAGE_NAME` は `lib`, `client`, `server` のいずれかとする。

`Github Actions` で `publish` する方法は試してないです。

シンプルな競合対象としては、`npm link` や `git submodule` などでの結合だと思う。

とりあえず、手元で試しに導入してみたのでメモを書き殴ります。

# [lerna](https://github.com/lerna/lerna) 

複数パッケージを一つのリポジトリで管理運用するためのツール。

コミットを元に、サブディレクトリ毎に `package` として `publish` するなど。

パッケージの依存関係を一括で解決する `lerna bootstrap` の機能については、[Yarn Workspaces](https://classic.yarnpkg.com/en/docs/workspaces) を利用することにした。

# [Yarn Workspaces](https://classic.yarnpkg.com/en/docs/workspaces)

複数のパッケージで依存関係を一括管理する機能。

依存関係の共有による最適化と、競合問題の回避を謳っている。

`yarn` は使っていて、覚えることが少なそうなので採用。

# 最終的なフォルダ構成

`lib`, `client`, `server` という三つの構成を一つのリポジトリに入れて、`client` と `server` から `lib` を参照する構成を例として想定した。

```
- GITHUB_REPONAME/
    - packages/
        - lib/
            - package.json
        - client/
            - package.json
        - server/
            - package.json
    - .npmrc
    - lerna.json
    - package.json
```

`https://github.com/GITHUB_USERNAME/GITHUB_REPONAME` にリポジトリが保存されて、

`@GITHUB_USERNAME/lib`, `@GITHUB_USERNAME/client`, `@GITHUB_USERNAME/server` という三つのパッケージが作成され、`npm` パッケージと同様に参照可能。

# リポジトリの用意

`github` に新しいリポジトリを用意して、手元に `git clone` して、そこのフォルダで作業する。いつも通り

```bat
git clone https://github.com/GITHUB_USERNAME/GITHUB_REPONAME
cd GITHUB_REPONAME
```

# `Yarn Workspaces` のために `packages.json` の設定

`"private": true` の設定と、`"workspaces"` 以下の設定を追加。

```packages.json
{
  // 略
  "private": true,
  "workspaces": {
    "packages": [
      "packages/*"
    ]
  },
```

# `lerna` のインストール

## インストール

[lerna](https://github.com/lerna/lerna) 公式のドキュメントには、いきなり `npx lerna init` せよと書かれているが、手元の環境では `graceful-fs` 関係のエラー（`cb.apply is not a function`）が出て動作しなかったので、`yarn global add lerna` してから使い始めた。

```bat
yarn global add lerna
yarn lerna init
```

（追記）[npx で 「cb.apply is not a function」 エラーが出る](https://let.blog.jp/article/24362797) を参考に、npx 内での graceful-fs の polyfill を無効化したら通るかも知れない

## `lerna.json` の設定

設定内容の概略は以下の通りと認識している

- 習わしに倣って `packages/` 以下にソースコードを置くことにしたので、`"packages": ["packages/*"]`
- `yarn` を使うので `"npmClient": "yarn"`
- `yarn workspaces` を使うので `"useWorkspaces": true`
- パッケージ毎にバージョンを設定したいので `"version": "independent"`
- [LernaとYarn WorkspacesでMonorepo管理](https://blog.cybozu.io/entry/2020/04/21/080000) に倣って command を追加したが、まだ十分に理解していない。`lerna publish` で `main` ブランチを `GitHub Packages` に `publish` する。その際には `conventionalCommits` でコミットログが書かれているものとして扱うというそのままの認識で良いと思う。

```lerna.json
{
  "packages": ["packages/*"],
  "npmClient": "yarn",
  "useWorkspaces": true,
  "version": "independent",
  "command": {
    "publish": {
      "conventionalCommits": true,
      "message": "chore(release): publish",
      "registry": "https://npm.pkg.github.com",
      "allowBranch": "main"
    }
  }
}
```

# 全体で共通して利用する `npm` パッケージのインストール

[`Yarn Workspaces`](https://classic.yarnpkg.com/en/docs/workspaces) を利用した。`yarn workspaces` は `yarn -W` で省略可能。`-W` or `workspaces`) が付く以外はいつも通りに `add` / `remove` すればいい。

```
yarn -W -D add typescript @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-plugin-import lerna
```

（良く分かってないメモ）この後、パッケージ毎にも `npm` パッケージをインストールするが、`yarn workspaces` の場合、それらも`hoisting（巻き上げ） ` という機能によって、`GITHUB_REPONAME/node_modules` 以下に集約される模様。`lerna` 単体だと、パッケージ毎のものは個別に入るのかも。便利だけど、いかにもトラブりそうな機能で怖い。

# パッケージ毎の設定

パッケージ毎の `packages.json` を作成する。

ディレクトリは `lerna` のコマンドで作成しても良いが、ここでは `yarn workspaces` を利用するので、普通にパッケージのディレクトリを作って、`packages.json` を置けば良い。`create-react-app` などでも良いはず。

## `packages.json` の 設定

### `name` に `scope` を付ける（GitHub Packages 用）

`name` は `@GITHUB_USERNAME/PACKAGE_NAME` の用に、`github` のユーザ名を `scope` として付与する。これは、`GitHub Packages` の為に必要。組織で利用する場合は org名 を（https://github.com/ の後ろと一致させる必要があるという認識）。

`name` に `GITHUB_REPONAME` も入れたかったのだけど、`scope` についてはユーザ名と揃える必要があった。埋め込むなら `PACKAGE_NAME` に埋め込むことになりそう。

### `repository` と `publishConfig`

パッケージのリポジトリと `publish` 先を `GitHub Packages` に設定する。デフォルトは `npmjs` なので不安なら、`npmjs` からログアウトするなどしておくこと。

リポジトリ全体を `private` にした環境でしか試行していないが、 `"access": "restricted` にしておくと `private` になるのかも。

`"private": true` を設定していると、`packages` として `publish` されないので、設定に不安がある間は `"private": true` にしておくと良いかも。

```packages/PACKAGE_NAME/packages.json
{
  // 前略
  "repository": {
    "type": "git",
    "url": "ssh://git@github.com/GITHUB_USERNAME/GITHUB_REPONAME.git",
    "directory": "packages/PACKAGE_NAME"
  },
  "publishConfig": {
    "access": "restricted",
    "registry": "https://npm.pkg.github.com/"
  }
  // 後略
}
```

### 依存関係

`dependencies`, `devDependencies` もいつも通り。編集して `yarn` なり、必要に応じて `yarn add` / `yarn add -D` すればいい。パッケージ直下の `node_modules` ではなく、ルートの `node_modules` にインストールされる辺りの挙動が異なる。

`lerna` だけを利用する場合は、この辺りが異なるはず。[`Yarn Workspaces`](https://classic.yarnpkg.com/en/docs/workspaces) はお手軽そうだった。

個別に入れたい場合は、[nohoist オプション](https://classic.yarnpkg.com/blog/2018/02/15/nohoist/) を利用する。

# GitHub の認証情報を `.npmrc` に埋め込む

[GitHub の Settings Developer settings / Personal access tokens]
(https://github.com/settings/tokens) で `Personal access token` を発効する。

名前は分かりやすい名前を付ければOK。

権限は、パッケージの作成・編集が必要なので `Select scopes` で `repo`, `write:packages`, `read:packages`, `delete:packages` を付与した。

パッケージの読み出しだけなど、利用者に応じて権限は調整すること。

作成するとトークンに対応したパスワードが表示される。以下ではそのパスワードを `TOKEN_SECRET` とする。

ルートに、以下の内容の `.npmrc` を作成する。ユーザ名とシークレットは置き換えること。

```.npmrc
@GITHUB_USERNAME:registry=https://npm.pkg.github.com/
//npm.pkg.github.com/:_authToken=TOKEN_SECRET
```

- 一行目が、 `@GITHUB_USERNAME` スコープのパッケージが `GitHub Packages` にあることの設定
- 二行目が、`GitHub Packages` の認証情報
- 三行目は、動いた際のおまじないをそのまま残している

`npm login` コマンドを利用する方法だと、`C:\Users\username\.npmrc` (`~/.npmrc`) に保存される。それでも大丈夫なはずなのだけど、試行錯誤の最中に消したので分からない。

`TOKEN_SECRET` が流出するとリポジトリに無制限にアクセスされてしまうので、作成後は、`.gitignore` に追加するのをお忘れ無く。

```.gitignore
.npmrc
```

# `libs` を作成する

`npm` パッケージを作る要領で、`libs` 以下にコードを書いていく。

共通設定はルートの `tsconfig.common.json` に記述して、`lib` 用の設定から読み込んだ。`tsconfig.json` をまるっと貼っておく。

```packages/lib/tsconfog.json
{
  "extends": "../../tsconfig.common.json",
  "compilerOptions": {
    "outDir": "lib",
    "rootDir": "src",
    "baseUrl": "src",
    "module": "commonjs",
    "moduleResolution": "node",
    "declaration": true
  },
  "include": ["src"]
}
```

# `publish` する

`git` コミットしておく。`push` は自動でしてくれる。

`yarn lerna publish minor` を実行すると、更新されているパッケージの `X.Y.Z` の `Y` の部分がインクリメントされてパッケージ化される。バージョンは `minor` のオプションを変えればコントロール出来る。major なら X、patch なら Z。

当然だが、`.npcrc` が参照できるフォルダ（つまりルートなど）で実行しないと認証がこける。`~/.npcrc` に書く場合は気にしなくて良いが。

上手く packages が `publish` された場合、`https://github.com/GITHUB_USERNAME?tab=packages&repo_name=GITHUB_REPONAME` にパッケージができている。もしくは、`https:://github.com/GITHUB_USERNAME/GITHUB_REPONAME` の右列に `Packages` が増えている。

エラーが出た場合、随時、エラーメッセージに対応する。大体、`C:\Users\username\AppData\Roaming\npm-cache\_logs` のログに書かれている内容に対応していけば解決出来た。

## 記憶にあるトラブルシューティング

各パッケージの下に `LICENCE.md` なども置いておくと `publish` 時に `lerna` に怒られない。

### `org.couchdb.user' is not in the npm registry.`

`.npmrc` の記述間違いだったような… `token` を作り直したような、試行錯誤をした記憶があるが、`npm login` コマンドを使わずに `.npmrc` を手動で作って解決したと思う。

### `This command requires you to be logged in.`

- `.npmrc` の記述が間違っており、`npmjs` を見に行こうとしていた
- C:\users\username\.npmrc を消して、パッケージ直下に .npmrc を作り直したなど

### エラーが無かったのにパッケージができなかった。

`"private": true` を指定していたのでパッケージされなかった。ログには `verbose stack Remove the 'private' field from the package.json to publish it.` などと出力されている。

### `npm package "PACKAGE_NAME" does not exist under owner "OTHER_SCOPE"`

`scope` の部分が `GITHUB_USERNAME` と一致していなかった。

### `packages.json` に `gitHead` の項目が残ってしまう

認証エラーなどで `publish` に失敗すると、中間で書き換えられた内容が書き戻されずに、以降で `gitHead` が残ってしまうことがある。

全部の `packages.json` から `gitHead` を削除して `publish` し直すことで以降消えるようになった。

# `lib` を利用する

`server` / `client` で `packages.json` に依存関係を指定する。その際にパッケージ化されているバージョン名を指定しないと駄目みたい。

```packages/server/packages.json
{
  // 略
  "dependencies": {
    "@GITHUB_USERNAME/lib": "^0.6.0",
  },
```

後は、`npm` パッケージと同様に `import` なり `require` なりで利用できるはず。

```packages/server/lib/hoge.ts
import { FooBar } from "@GITHUB_USERNAME/lib";
```

## `No matching version found for @GITHUB_USERNAME/lib@^0.8.0.`

バージョン番号の不一致をチェックする。

## `Couldn't find package "@GITHUB_USERNAME/lib" on the "npm" registry.`

`.npmrc` の内容を見直すと良さそうだった。`GitHub Packages` を見に行っていない可能性が大。

# Cloud Functions からも利用する

[サーバ側で `Cloud Functions` を利用したかったので、`.npmrc` を `servers/.npmrc` にコピーした。](https://firebase.google.com/docs/functions/handle-dependencies#using_private_modules)

他には特別な設定は必要なく、`firebase deploy --only functions` の実行時間が延びてしまったぐらいで、上手く参照してくれているとおもう。

ライブラリの作り方が不味いなど `lerna` と `yarn workspaces` には無関係のエラーは起こしていたが、それは別の問題。

# 新規に git clone した後

`yarn` を実行すると、全パッケージの依存関係を全て解決してくれる。`node_modules` 以下を消した場合なども同様。

`npm` を使う場合は、`yarn lerna bootstrap` を実行した後、パッケージ毎に `npm install` になるっぽい。

# 参照したページ

感謝。

- [Yarn Workspaces 公式](https://classic.yarnpkg.com/en/docs/workspaces)
- [lerna github](https://github.com/lerna/lerna)
- [lerna 公式](https://lerna.js.org/)
- [依存関係の扱い](https://firebase.google.com/docs/functions/handle-dependencies)
- [LernaとYarn WorkspacesでMonorepo管理](https://blog.cybozu.io/entry/2020/04/21/080000)
- [How to use Firebase Cloud Functions and Yarn Workspaces](https://medium.com/gdgeurope/how-to-use-firebase-cloud-functions-and-yarn-workspaces-24ca35e941eb)
- [Publishing and Installing Private GitHub Packages using Yarn and Lerna](https://viewsource.io/publishing-and-installing-private-github-packages-using-yarn-and-lerna/)
- [Yarn workspaces から Lerna に移行した](https://qiita.com/acro5piano/items/c2a8dd3a75c99b704548)
- [yarn workspaceでmonorepo+TypeScript+Lint環境をつくる](https://qiita.com/suzukalight/items/0b22f11ad05308f638a6)
- [lernaでのmonorepoにおけるリリースフロー(Fixed/Independent)](https://efcl.info/2019/01/26/monorepo-release-flow/)
- [GitHub Packagesで社内用npmパッケージを配布して使ってみた](https://zenn.dev/odiak/articles/950f05a34e9c84)
- [Structuring a Firebase web project with Lerna](https://medium.com/firelayer/structuring-a-firebase-web-project-with-lerna-ab6b5ea8e1f8)
- [モノレポについての誤解 - Misconceptions about Monorepos: Monorepo != Monolith を翻訳しました](https://www.graat.co.jp/blogs/ck1099bcoeud60830rf0ej0ix)
- [Lernaを使ってFirebase環境のためのモノレポ環境一式をカッコよく構築する](https://qiita.com/kyasbal_1994/items/e9c5a918f4d2ddf10a28)
- [
[question] all my package.json's now contain gitHead key. What to do about it?](https://github.com/lerna/lerna/issues/1880)