private npm registryの移行ノウハウ

この記事は[ドワンゴ Advent Calendar 2023](https://qiita.com/advent-calendar/2023/dwango)の21日目の記事です。

フロントエンド開発をしている方は、何かしらnpm registryを利用していると思います。
特に業務などで内製のパッケージを作った場合はprivate registryに置いて利用することが多いはずです。

本記事は、private npm registryを別のところに移行する際の方法についてお話します。

# 背景
private npm registryとしてはnpmjs、Github Packages Registry、AWS Codeartifactといったクラウドのものから、Sinopia, Verdaccioなど自分でサーバを立てて運用するものなど様々なものがあります。

一つのレジストリをずっと使い続けられれば良いですが、何らかの事情により別のレジストリへ移行しなければならない状況が発生することもあります。
扱うリポジトリやパッケージが少なく、1, 2日で移行が完了する場合はnpmrcやregistry周りの設定をササっと書き換えれば済む可能性も高いですが、移行対象のパッケージ数が多く、かつパッケージ間の依存関係がある場合、日々の開発作業を止めずに移行するのは難しくなってきます。

そのような状況で単にnpmrcや周辺の設定を切り替えるだけでは済まない場合はどのように移行を進めれば良いのでしょうか。

# 単純な移行で問題となる点
単に1つずつレジストリ設定を書き換えていくとどのような問題が起こるか。

## 基本的に、1つのスコープで複数のレジストリを運用できない
例として以下のような依存構造を考えてみます。
`@example/lib-a`, `@example/lib-b`, `@example/core` というパッケージがあり、`lib-a`, `lib-b`は`core`に依存していたとします。

![screenshot.1702465958.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540681/d2a5975a-2a6f-81d6-c4e5-0edce7f88461.png)


この時、`core`のレジストリが `registry-2` に切り替わると、それ以降のバージョンは`registry-2`にのみpublishされるため、`registry-1` を使っている`lib-a`, `lib-b`からは `core` の最新版が取れず更新が滞ってしまいます。

パッケージ間の依存がなかったり、`lib-a`, `lib-b`のレジストリをすぐに`registry-2`に切り替えられる場合はあまり大きな問題になりにくいですが、長期にわたると必要な変更がすぐに反映できなかったり、マイグレーションコストも高くなってきます。

この問題は、スコープ毎にレジストリの設定を分けられる（≒同じスコープであれば同じレジストリを使うという前提の）仕様によって起こるものですが、スコープはパッケージ名で決まるのでパッケージ名の変更容易性が移行難易度に影響します。

上記例でいえば、`core` の**スコープを別のものにして、レジストリ設定も分けることができれば移行作業がシンプル**になります。

![screenshot.1702465991.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540681/7c930666-c8d8-5ed7-f8c9-4a919b81e175.png)

# スコープを変えずに移行する
スコープを変えることができる場合は、順次新しいスコープ、レジストリへ移行していき、参照する側も新しいものへ切り替えていけば時間はかかってもシンプルな手順で移行を進めることができます。
ではスコープを変更することができない場合を考えてみます。

前述の通り、単純に切り替えていってしまうと最新版の取得ができなくなるため、移行に時間がかかる見込みがある場合は**移行前と移行後両方のレジストリにパッケージがある必要があります**。

## 1つのパッケージを複数のレジストリへ配置
移行期間中は両方のレジストリへパッケージを配置するとした場合、それを実現する方法はいくつかあります。
ここでは3つ挙げますが、結論から言えば3つ目がオススメです。

- 移行後のレジストリのupstreamとして移行前のレジストリを指定する
- リリース時に両方のレジストリへpublish[^1]する
- 片方のレジストリへpublishした後、そのパッケージのアーカイブをもう片方のレジストリへアップロードする

[^1]: 本稿でいうpublishとは、npm publishを始めとする、パッケージをpackし、レジストリへメタ情報も含めてアップロードする各種ツールの操作を指します

### 移行後のレジストリのupstreamとして移行前のレジストリを指定する
これはできるレジストリや条件が限られると思いますが、移行後のレジストリが存在しないパッケージはupstreamから取得するようにできれば、シンプルに移行が可能です。


<img width="600" alt="20723896-0814-45df-9968-3c91193981fa.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540681/20723896-0814-45df-9968-3c91193981fa.png">

### リリース時に両方のレジストリへpublishする
1つ目のレジストリへpublishした後に、もう1つのレジストリへ再publishする方法です（便宜上、以後multi-publishと呼称します）。
両方のレジストリへ配置するとなった場合にすぐ思いつきそうな方法ですが、リポジトリの設定やリリースの仕方によって対応が異なる場合があります。

詳細は後述しますが、あまりオススメしません。

<img width="400" alt="51ff8bb3-ed2e-a396-0a6e-3d02b01d0865.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540681/51ff8bb3-ed2e-a396-0a6e-3d02b01d0865.png">

### 片方のレジストリへpublishした後、そのパッケージのアーカイブをもう片方のレジストリへアップロードする
3つのうちでは一番確実で難易度も低い方法に感じます。

具体的には、移行前のレジストリへpublishした後、そのレジストリからアーカイブをダウンロードし、移行後のレジストリへアップロードします（逆方向の同期でもOK）。

直接アーカイブをダウンロード、アップロードできないレジストリだと使えない方法ですが、大抵のレジストリは対応しているのではないかと思います。

この方法の場合、単一の仕組みで両方のレジストリへ配置できるので、publishやリリースの方法に左右されず作業コストが下がります。
なので個人的には1番オススメな方法です。

<img width="400" alt="d828218c-b20a-215b-434b-7f23f95a6157.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540681/d828218c-b20a-215b-434b-7f23f95a6157.png">

## 両方のレジストリへpublishする際の問題
先ほどオススメしない方法として、リリース時に両方のレジストリへpublishする方法を挙げましたが、これがなぜ避けた方がいいのかを具体的に書きます。

### パッケージマネージャによって対応が異なる
npm, yarn, pnpmなどパッケージマネージャの種類によって使い方が異なるので、リポジトリによって使っているパッケージマネージャが異なると各々の対応が必要になります。

例えば、npmの場合は `npm publish` した後に、別のレジストリへpublishするため `npm publish --@example:registry=https://~` というコマンドを実行しますが、
yarnの場合は`yarn publish`した後に環境変数 `YARN_REGISTRY`で別のレジストリへ変えたり、configを変更してから`yarn publish`し直す、というようにツールによって対応方法が変わってきます。

特にスコープ付きの場合、CLIでレジストリの向き先を簡単に変更できないことが多いので、`.npmrc` 等を書き換えてpublishするという複雑な対応が必要になります。

ちなみに、[nrm](https://github.com/Pana/nrm) や [yrm](https://github.com/i5ting/yrm) というレジストリ切り替えツールもあるので使えるかもしれないです（私は未検証です）。

### publish以外の操作も一緒にやるツールを使っていると対応が複雑になる
semantic-releaseやlernaなど、バージョニングの処理も含めてpublishするツールを使っている場合、2重にバージョンアップをしないようにする必要があり、`lerna publish`した後に`npm publish`をするといった対応が必要になります[^2]。

[^2]: publishの互換性を保つためにlerna publishを2回やらないといけない場合はさらに複雑になります

特に、yarn v3 + lernaの構成の場合、lernaはpublish時に（`"npmClient": "yarn"`の設定を入れていても`.yarnrc.yml`ではなく）`.npmrc`を参照する[^3]ので罠っぽい挙動にも繋がります。

[^3]: https://lerna.js.org/docs/features/version-and-publish

### リポジトリによって作法が異なると各対応が必要になる
上記のように、ツールの仕様に合わせた対応が必要になりますが、移行対象のすべてのリポジトリが同じツール、仕組みであれば対応方法も少なくて済みます。

しかし、リポジトリによってlernaを使っていたり、semantic-releaseを使っていたり、シンプルにnpmだけ使っていたり、というようにバラバラな仕組みだと各種対応が必要で非常に大変です。

こういった理由から複数のレジストリへpublishする仕組みを使うのは避けた方が良いでしょう。

# どうしてもスコープを変えずにmulti-publishしないといけない！というあなたへ
ここまで読めばnpmレジストリ移行をしようとしている方はmulti-publishを選択するとは思えないですが、それでもこの方法をとらなければならない方もいるかもしれません。

そんな方のために、役に立つかもしれないTipsを載せておきます。

## lernaの `--registry` オプションは、スコープ付きのパッケージのレジストリ切り替えに使えない
`--registry`はスコープ指定なしのレジストリを変更するオプションです。
したがって、`.npmrc` に以下のような記述があったとすると、`--registry` オプションで変更されるのは `registry`のconfigであり、`@example:registry`ではないため、適切にレジストリの向き先が変えられません。

```
registry=https://registry.npmjs.org/
@example:registry=https://example.com/npm/
//example.com/npm/:_authToken=~(以下略)
```

スコープ付きのレジストリ設定をCLIオプションで指定できないようなので、この場合は `npm config set`などを使って`.npmrc`を書き換えて対応します。



## lerna使用時、プロジェクトディレクトリの`.npmrc`を書き換えると `uncommitted changes` のエラーでpublishできなくなってしまう
lernaはgitの差分があるとpublishに失敗するため、プロジェクトディレクトリの`.npmrc`を書き換える方法でレジストリ変更をする場合、ダミーのコミットをしてからpublishするという対応が必要になります。

[ignoreできるようにして欲しいという提案](https://github.com/lerna/lerna/issues/2329) も出ているようですが、対応される見込みはなさそうです。

yarn v3以降を使っている場合、`.npmrc` をバージョン管理の対象にせず、プロジェクト内では`.yarnrc.yml` のみを管理し、`.npmrc`を変更してもlernaに怒られないようにするという回避策は可能です。


## semantic-release使用時は、`npm publish`でmulti-publishする
semantic-releaseはlernaほど複雑ではないので、1つ目のレジストリにsemantic-releaseでpublishした後、 `npm publish --@scope:registry=https://~` などでmulti-publishが可能です。

# さいごに
文字ばかりになってしまいましたが、private npm registryの移行を考えている方の一助になれば幸いです。

今回紹介した方法以外でも良い移行方法はあるかと思いますし、良いアイディアがある方はぜひコメント等残していっていただきたいです。
（意見や間違いの指摘も歓迎です）

それではまた。