【TypeScript】バックエンド・フロントエンドで共有できるプライベートGithubPackagesの作り方

## 🌟はじめに
本記事はプロもくチャット Adevent Calendar2023の5日目です

https://qiita.com/advent-calendar/2023/puromoku

## 🤔 なんで必要なの？
### 🔥 Firestoreを使っている
僕が所属しているプロダクトでは、下記のような構成を取っています
- フロントエンド: Vue3/TypeScript
- バックエンド: Node.js/TypeScript
- インフラ: Firebase/GCP

### 📚 Firestoreはカスタムオブジェクト経由でRead/Writeできる
DBは基本的にはFirestore、一部CloudSQLを使っています。
フロントエンドから直接Firestoreの読み書きする部分が多く、随所でバックエンド経由でFirestoreへアクセスするため、フロントエンド・バックエンドともに同じドキュメントやコレクションにアクセスするのが多いです。

FirestoreはNoSQLのため基本的にはスキーマがありませんが、下記のようにモデルクラスと変換メソッド（`toFirestore`, `fromFirestore`）を定義することでカスタムオブジェクトを使って保存・読み取りすることができます。
https://firebase.google.com/docs/firestore/query-data/get-data?hl=ja#custom_objects
※このあたりの解説は今回は割愛します

### 🌐 共有したい、でも普通にnpmパッケージ化すると全世界に公開されてしまう…
上記のような状態なので、モデルクラスや変換メソッドをフロントエンド・バックエンドで共有する必要が出てきました。
別リポジトリをシンボリックリンク化してデプロイしてゴニョゴニョ…してましたが、デプロイが遅くCIのコードもかなり複雑化するため、npmパッケージ化したいと考えました。
が、npmパッケージは全世界に公開されてしまうため、ちょっと微妙…
そこでGithubPackagesを使ってプライベートなnpmパッケージを作成しそれらをVue, Node.jsそれぞれで`npm install`して使うようにします。


## 🎯 ゴール
- モデルクラス・変換メソッドをGithubのプライベートリポジトリに作成する
- プライベートリポジトリのコードをGithubPackagesにパッケージを公開する
- 公開されたパッケージをVue3/Node.jsそれぞれで`npm install XXX@YY.YY.YY`のような形で利用することができる


## 📝 本題
### 🛠️ モデルクラスをGithubのプライベートリポジトリに作成する
- さくっと共有したいリポジトリを作成します。
  ```
  npm init -y
  npm install typescript -D
  npx tsc --init
  ```
- `.gitignore`を作成（最低限しか入れてません）
  ```
  node_modules
  .DS_Store
  Thumbs.db
  dist/**/*
  ```
- 共有したいコードを作る
  ```src/index.ts
  export class City {
    constructor(
      readonly name: string,
      readonly state: string,
      readonly country: string
    ) {}
    static toFirestore(city: City) {
      return {
        name: city.name,
        state: city.state,
        country: city.country,
      };
    }
  }
  ```
- `tsconfig.json`を設定
  僕の担当するプロダクトではNode.jsはCommonJS、VueはESModuleで動いているため、それぞれの形式でビルドする必要があります。このあたりの詳しい話は割愛します。
  下記のような形でtsconfigをCommonJS用、ESModule用に作成します。(ベースとなるtsconfigはデフォルトのままです)
  ```tsconfig.cjs.json
  {
    "extends": "./tsconfig.json",
    "compilerOptions": {
      "module": "CommonJS",
      "outDir": "./dist/cjs"
    },
    "include": ["src/**/*"],
    "exclude": ["node_modules"]
  }
  ```
  ```tsconfig.esm.json
  {
    "extends": "./tsconfig.json",
    "compilerOptions": {
      "module": "esnext",
      "outDir": "./dist/esm"
    },
    "include": ["src/**/*"],
    "exclude": ["node_modules"]
  }
  ```
- `npm script`でビルドできるようにする
  ```package.json
  {
    ...
    "scripts": {
      "build:esm": "tsc -d -p tsconfig.esm.json",
      "build:cjs": "tsc -d -p tsconfig.cjs.json"
    },
    ...
  }
  ```
- `npm run build:esm`, `npm run build:cjs`で、distディレクトリに出力されることを確認
- Common.js, ESModule用にそれぞれエンドポイントを定義
  ```package.json
  {
    ...
    "main": "dist/cjs/index.js",
    "module": "dist/esm/index.js",
    "types": "dist/esm/index.d.ts",
    ...
  }
  ```

ここまでで、ファイル構成は下記のようになっていると思います
```
private-common % tree
.
├── dist
│   ├── cjs
│   │   ├── index.d.ts
│   │   └── index.js
│   └── esm
│       ├── index.d.ts
│       └── index.js
├── node_modules
├── package-lock.json
├── package.json
├── src
│   └── index.ts
├── tsconfig.cjs.json
├── tsconfig.esm.json
└── tsconfig.json
```

### 📦 プライベートリポジトリのコードをGithubPackagesにパッケージを公開する
- PATを作成
  - プライベートなパッケージの作成・インストールをするには、GithubのPersonal Access Token（PAT）を作成し、ローカルに配置する必要があります。
作り方はこのあたりを参照してください
https://docs.github.com/ja/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token
  - `write:packages`権限をつけてあげてください
- パッケージ名を定義
  プライベートとして発行するには`Github名 or Oraganization名/リポジトリ名`とする必要があります。これを下記のようにpackage.jsonに定義します。
  ```package.json
  {
    "name": "@IkumaHayashi/private-common",
    ...
  }
  ```
- `.npmrc`ファイルを作成
  Github Packagesは、通常のnpmレジストリとは別の場所（`https://npm.pkg.github.com`）に公開されます。
  このレジストリにプライベートなパッケージを公開するため、下記2点の情報を`npm`コマンドに伝えてあげる必要があります
  - レジストリの場所（`https://npm.pkg.github.com`のこと）
  - プライベートなパッケージをインストールするための秘密鍵（PATのこと）
  
  この2点が載っているファイルを`.npmrc`として作成することで、`npm`が自動的にこのファイルの情報を読み取り、パッケージの公開やインストール時に使用します。
  ただし、PATはリポジトリ管理するわけにはいきません。そこで、プロジェクト内ではレジストリの場所を、各開発者のホームディレクトリにはPATを配置します。
  下記のようにファイルを作成します。
  ```プロジェクトルート/.npmrc
  @あなたのGithub名もしくはOraganization名:registry=https://npm.pkg.github.com
  ```
  ```~/.npmrc
  //npm.pkg.github.com/:_authToken=PAT
  ```
- `.npmignore`を空ファイルで作成
  - `.npmignore`はパッケージを発行する際にどのファイルを無視するかを指定するファイルです。
  - npm v9から、`.npmignore`がない場合暗黙的に`.gitignore`が使用されます。今回の構成では`dist`ディレクトリをパッケージとして発行したいため、このままではsrcディレクトリなどしか発行されず、使うことができません。
  - そのため空の`.npmignore`を定義して除外するファイルをなくすことで、`dist`ディレクトリを含めてあげます。

- 発行
  パッケージの発行は`npm publish`で行えます。`--dry-run`をつけると、発行前にどういう内容で発行されるか確認することができます。
  ```
  npm notice 
  npm notice 📦  @IkumaHayashi/private-common@0.0.1
  npm notice === Tarball Contents === 
  npm notice 293B   dist/cjs/index.d.ts
  npm notice 428B   dist/cjs/index.js  
  npm notice 293B   dist/esm/index.d.ts
  npm notice 314B   dist/esm/index.js  
  npm notice 599B   package.json       
  npm notice 264B   src/index.ts       
  npm notice 174B   tsconfig.cjs.json  
  npm notice 172B   tsconfig.esm.json  
  npm notice 12.3kB tsconfig.json      
  npm notice === Tarball Details === 
  npm notice name:          @IkumaHayashi/private-common            
  npm notice version:       0.0.1                                   
  npm notice filename:      @IkumaHayashi/private-common-0.0.1.tgz  
  npm notice package size:  4.5 kB                                  
  npm notice unpacked size: 14.8 kB                                 
  npm notice shasum:        87328bb7206b4661afce03868d8e888ded15194a
  npm notice integrity:     sha512-B3FeOLAppTO3w[...]cb/qtu6kFfrUg==
  npm notice total files:   9                                       
  npm notice 
  npm notice Publishing to https://npm.pkg.github.com (dry-run)
  + @IkumaHayashi/private-common@0.0.1
  ```

  チェックすべきは下記の２点です
  - パブリッシュ先が`https://npm.pkg.github.com`になっている
  - 発行されるファイル（`Tarball Contents`）に`dist`ディレクトリが含まれている

  上記が問題なければ`npm publish`で実際に発行します。
  ```
  private-common % npm publish
  npm notice 
  npm notice 📦  @IkumaHayashi/private-common@0.0.1
  npm notice === Tarball Contents ===
  ... 
  npm notice Publishing to https://npm.pkg.github.com
  + @IkumaHayashi/private-common@0.0.1
  ```

  問題なく発行されました

発行されるとこんな感じでGithubのリポジトリに表示されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276499/f20776ff-04ae-e099-7647-f5e491bd503b.png)

### 🚀 インストールしてみる
- 使用するリポジトリに`.npmrc`ファイルを作成
  ```プロジェクトルート/.npmrc
  @あなたのGithub名もしくはOraganization名:registry=https://npm.pkg.github.com
  ```
- `npm install @IkumaHayashi/private-common@0.0.1`


## 💡 補足
### 🤖 Github Actionsで`npm install`するとコケる😭
GithubのPATがないことが原因だと思います。ただ、GithubActionsではルートディレクトリにファイルを作成することができません。
そんなときは、npm installするステップで環境変数にNODE_AUTH_TOKENにPATを定義してあげるとインストールできます
```
      - name: 依存関係をインストール
        run: |-
          npm install
        env:
          NODE_AUTH_TOKEN: ${{ secrets.PAT }}
```

## 🙏 最後に
ここまで読んでくださりありがとうござました！
ちょっと時間がなくてインストール部分は確認できておりません… :bow: 
コメントいただければ確認・修正行いますのでお気軽にコメントしてください🙆‍♂️