【Yarn】パッケージをオフラインでインストールする

# 背景
[Yarn v2についての動画](https://www.youtube.com/watch?v=XePfzVs852s)で出てきた「v1のオフラインインストール」について、よく知らなかったので調べてみた。

# 概要
Yarnのオフラインインストールとは、パッケージを追加する際に実態のtarballをローカルにダウンロードしておき、後からそれを用いてパッケージをインストール出来るようにしたものである。パッケージをインストールする際に `yarn.lock` ファイルにダウンロード元のURLが記録されているため、URLが有効な限りは何度実行しても同一のファイルがダウンロードされることが保証されるが、ダウンロード元のダウンや閉鎖等によってURLが無効になった場合はダウンロードに失敗する。このような場合に備えて予めtarballをダウンロードしておき、オフライン時に利用するのがオフラインインストールである。

# 詳解
## 事前準備
まずはじめに、適当なディレクトリを作成して以下のファイルを配置する。

```json:package.json
{
  "name": "yarn-offline",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "is-array": "^1.0.1",
    "left-pad": "^1.3.0",
    "mime-types": "^2.1.27"
  }
}
```

パッケージを追加し、ディレクトリの構成が以下の通りになっていることを確認する。

```bash
$ yarn
（略）
$ ls
node_modules    package.json    yarn.lock
```

## オフラインミラーの設定
オフラインミラー（ダウンロードされるtarballが配置されるパス）を設定する。ここで指定するパスは**ホームディレクトリからの相対パス**であることに留意する。

```bash
$ yarn config set yarn-offline-mirror ./npm-packages-offline-cache
yarn config v1.22.0
success Set "yarn-offline-mirror" to "./npm-packages-offline-cache".
✨  Done in 0.04s.
$ yarn config set yarn-offline-mirror-pruning true
yarn config v0.23.2
success Set "yarn-offline-mirror-pruning" to "true".
✨  Done in 0.06s.
```

`yarn config set` で追加した設定は `~/.yarnrc` に記録されている。これを特定のプロジェクトでのみ利用可能にするため、ファイルをプロジェクトディレクトリ内に移動する。

```bash
$ mv ~/.yarnrc ./
```

## パッケージのダウンロード
オフラインインストールを用いる場合でも、`yarn.lock` は変更されないためパッケージ管理に影響を及ぼさない。これを検証するためにファイルをコピーしておく。

```bash
$ cp yarn.lock yarn.lock.before
```

`node_modules/` と `yarn.lock` を削除して再度パッケージの追加を行う。その後、`~/npm-packages-offline-cache` にtarballがダウンロードされていることを確認する。

```bash
$ rm -rf node_modules/ yarn.lock
$ yarn
yarn install v1.22.4
info No lockfile found.
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Saved lockfile.
✨  Done in 0.52s.
$ ls ~/npm-packages-offline-cache
is-array-1.0.1.tgz      left-pad-1.3.0.tgz      mime-db-1.44.0.tgz      mime-types-2.1.27.tgz
```

`yarn.lock` が変更されていないことを確認する。

```bash
$ diff yarn.lock yarn.lock.before
$ rm yarn.lock.before
```

# オフラインインストールの実行
オフラインインストールの検証を行うため、`node_modules/` とキャッシュを削除する。

```bash
$ rm -rf node_modules/
$ yarn cache clean
yarn cache v1.22.4
success Cleared cache.
✨  Done in 0.10s.
```

**ネットワークを切断してオフライン**にする。続けて `yarn --offline` を実行し、オフライン時でもパッケージが追加できることを確認する。

```bash
thara@ht-mbp-2:yarn-offline $ ls
package.json            yarn.lock
thara@ht-mbp-2:yarn-offline $ yarn --offline
yarn install v1.22.4
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
✨  Done in 0.55s.
thara@ht-mbp-2:yarn-offline $ ls
node_modules            package.json            yarn.lock
```

# 検証
上記に続けて、ダウンロード済みtarball、キャッシュ、`yarn.lock` の関係について検証する。

## オフラインインストールはキャッシュが無い場合に行われる
オフラインインストール時のパッケージとしてダウンロード済みtarballが用いられたように思えるがこれは正確ではない。キャッシュにもtarballが存在しており、こちらが優先して用いられるため「キャッシュが無ければダウンロード済みtarballが用いられる」という説明が正しい。以下を実行し、`~/npm-packages-offline-cache` が存在しなくともオフラインインストールに成功することを確認する。

```bash
$ rm -rf node_modules/
$ mv ~/npm-packages-offline-cache ~/npm-packages-offline-cache-renamed
$ yarn --offline
yarn install v1.22.4
info No lockfile found.
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Saved lockfile.
✨  Done in 0.28s.
```

キャッシュとダウンロード済みtarballの両方が存在しなければ、当然オフラインインストールは失敗する。

```bash
$ rm -rf node_modules/
$ yarn cache clean
yarn cache v1.22.4
success Cleared cache.
✨  Done in 0.05s.
$ yarn --offline
yarn install v1.22.4
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
error Can't make a request in offline mode ("https://registry.yarnpkg.com/is-array/-/is-array-1.0.1.tgz")
info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command.
```

## ダウンロード済みパッケージの検証には `yarn.lock` が用いられる
`~/npm-packages-offline-cache` を元に戻し、`yarn.lock` を削除してオフラインインストールに失敗することを確認する。`yarn.lock` が存在しない場合は、インストールするべきパッケージの検証が出来ないためである。

```bash
$ mv ~/npm-packages-offline-cache-renamed/ ~/npm-packages-offline-cache
$ rm yarn.lock
$ yarn --offline 
yarn install v1.22.4
info No lockfile found.
[1/4] 🔍  Resolving packages...
error An unexpected error occurred: "There should only be one folder in a package cache (got  in /Users/thara/.cache/yarn/v6/npm-is-array-1.0.1-e9850cc2cc860c3bc0977e84ccf0dd464584279a-integrity/node_modules)".
info If you think this is a bug, please open a bug report with the information provided in "/Users/thara/Desktop/yarn-offline/yarn-error.log".
info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command.
```

## キャッシュが存在する場合は `yarn.lock` は不要である
端末を**オンライン状態**にし、パッケージを追加してキャッシュを作成する。

```bash
$ yarn 
yarn install v1.22.4
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
✨  Done in 0.57s.
$ yarn cache list
yarn cache v1.22.4
Name       Version Registry Resolved                                                                                                
is-array   1.0.1   npm      https://registry.yarnpkg.com/is-array/-/is-array-1.0.1.tgz#e9850cc2cc860c3bc0977e84ccf0dd464584279a     
left-pad   1.3.0   npm      https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e     
mime-db    1.44.0  npm      https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92      
mime-types 2.1.27  npm      https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f
✨  Done in 0.04s.
```

端末を**オフライン状態**にして `node_modules/` と `yarn.lock` を削除し、オフラインインストールに成功することを確認する。

```bash
$ rm -rf node_modules/ yarn.lock
$ yarn --offline
yarn install v1.22.4
info No lockfile found.
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Saved lockfile.
✨  Done in 0.40s.
```