くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

pnpm x Turborepo x lerna-lite x GitHub Packagesでmonorepoなオレオレ非公開ライブラリをつくってみる

いろいろ作りたいけど、汎用的な処理とかUIをライブラリ化したいなと思って、
いろいろ調べたときの備忘録(*´ω`*)

構成

  • pnpm ... パッケージマネージャー。workspace機能を活用
  • Turborepo ... モノレポ向けのビルドシステム。依存関係を考慮してビルドしてくれる
  • lerna-lite ... バージョン更新とパッケージの公開
  • GitHub Packages ... パッケージのレジストリ

pnpm workspaces x Turborepoのmonorepo構成

ベースの部分はpnpm workspacesとTurborepoで構成

pnpm workspaces

まずは、package.jsonやworkspacesの設定

# rootのpackage.json作成
$ pnpm init

# workspace用の設定ファイルの作成
$ echo -e "packages:\n  - "packages/*"" > pnpm-workspace.yaml

# 各パッケージの作成と初期化
$ mkdir -p packages/lib-a packages/lib-b

# 各パッケージの初期化
$ cd packages/lib-a
$ pnpm init
$ cd ../lib-a
$ pnpm init

ここまでだとこんな感じ。

- packages
  - lib-a
    - package.json
  - lib-b
    - package.json
- package.json
- pnpm-workspace.yaml

Turborepo

$ pnpm -w add -D turbo

# turborepoの設定ファイルを作成
$ touch turbo.json

turbo.json

turbo.jsonの中身はこんな感じ。
使いたいscriptsコマンドを追加しておく

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "dev": {},
    "build": {}
  }
}

lib-*/package.json

各パッケージのpackage.jsonにもコマンドを追加

  {
  // ...
    "scripts": {
+     "dev": "echo \"DEV!! lib-a\"",
+     "build": "echo \"BUILD!! lib-a\"",
      "test": "echo \"Error: no test specified\" && exit 1"
    },
  // ...
  }

./package.json

rootのpackage.jsonturboコマンドを追加

  {
  // ...
    "scripts": {
+     "dev": "turbo dev --parallel",
+     "build": "turbo build",
      "test": "echo \"Error: no test specified\" && exit 1"
    },
  // ...
  }

コマンドの実行(依存関係なし)

依存関係無しで実行してみるとこんな感じ。
それぞれbuildコマンドを実行してくれる。

$ pn -w build
> example_monorepo@1.0.0 build ./example_monorepo
> turbo build

• Packages in scope: lib-a, lib-b
• Running build in 2 packages
• Remote caching disabled
lib-a:build: cache miss, executing 5c143b0237edae01
lib-b:build: cache miss, executing ac02900df05fa28a
lib-a:build: 
lib-a:build: > lib-a@1.0.0 build ./example_monorepo/packages/lib-a
lib-a:build: > echo "BUILD!! lib-a"
lib-a:build: 
lib-b:build: 
lib-b:build: > lib-b@1.0.0 build ./example_monorepo/packages/lib-b
lib-b:build: > echo "BUILD!! lib-b"
lib-b:build: 
lib-b:build: BUILD!! lib-b
lib-a:build: BUILD!! lib-a

 Tasks:    2 successful, 2 total
Cached:    0 cached, 2 total
  Time:    287ms 

2回目以降はキャッシュを使ってくれるのでさらに早い。

コマンドの実行(依存関係あり)

次にlib-alib-bを使う感じの依存関係を含めてみる。

$ cd packages/lib-a/
$ pn add -D lib-b

devDependencies:
+ lib-b 1.0.0 <- ../lib-b

これで実行してみると、lib-bから先に実行してくれる。

$ pn -w build
> example_monorepo@1.0.0 build ./example_monorepo
> turbo build

• Packages in scope: lib-a, lib-b
• Running build in 2 packages
• Remote caching disabled
lib-a:build: cache miss, executing 0a1e263ac434cb60
lib-b:build: cache hit, replaying logs ac02900df05fa28a
lib-b:build: 
lib-b:build: > lib-b@1.0.0 build ./example_monorepo/packages/lib-b
lib-b:build: > echo "BUILD!! lib-b"
lib-b:build: 
lib-b:build: BUILD!! lib-b
lib-a:build: 
lib-a:build: > lib-a@1.0.0 build ./example_monorepo/packages/lib-a
lib-a:build: > echo "BUILD!! lib-a"
lib-a:build: 
lib-a:build: BUILD!! lib-a

 Tasks:    2 successful, 2 total
Cached:    1 cached, 2 total
  Time:    325ms 

lerna-lite

各パッケージのバージョン変更/更新やパッケージの公開のために利用

# rootにインストール
$ pn -w add -D @lerna-lite/cli @lerna-lite/publish

# lerna-liteの初期設定
$ pn lerna init
$ cat lerna.json
{
  "$schema": "node_modules/@lerna-lite/cli/schemas/lerna-schema.json",
  "version": "0.0.0",
  "packages": ["packages/*"]
}

pnpmを使うので、lerna.jsonに設定を追加。

  {
    //...
    "version": "0.0.0",
+   "npmClient": "pnpm",
    "packages": ["packages/*"]
  }

各パッケージのバージョンと統一したいバージョンがあうように、
各パッケージのバージョンを一括で変更。

$ pnpm lerna version --no-git-tag-version --no-push 0.0.0 --yes
lerna-lite notice cli v2.4.2
lerna-lite info current project version 0.0.0
lerna-lite notice FYI git repository validation has been skipped, please ensure your version bumps are correct
lerna-lite info Assuming all packages changed
lerna-lite WARN version Skipping working tree validation, proceed at your own risk

Changes (2 packages):
 - lib-a: 0.0.0 => 0.0.0
 - lib-b: 0.0.0 => 0.0.0

lerna-liteは、gitのタグ付やpushもしてくれるけど、
いまはしたくないので、--no-git-tag-version --no-pushとつけておく

scriptにコマンドを追加

コマンドが長くなりがちなので、いくつかscriptsに追加。
version:setはさっき使ったのもの。

  • version:set: バージョンの設定
  • publish:latest: 最新版のリリース
  • publish:canary: カナリアリリース
  {
    // ...
    "scripts": {
      // ...
+     "version:set": "lerna version --no-git-tag-version --no-push",
+     "publish:latest": "lerna publish --yes --conventional-commits",
+     "publish:canary": "lerna publish --yes --canary --dist-tag beta --preid beta --conventional-commits",
      // ...
    },
    // ...
  }

最初のリリース(dry-run)

とりあえず、dry-runでお試し

# dry-run
$ pnpm publish:latest --no-git-tag-version --no-push --dry-run
Changes (2 packages):
 - lib-a: 0.0.0 => 0.0.1
 - lib-b: 0.0.0 => 0.0.1

$ git st -s
 M lerna.json
 M packages/lib-a/package.json
 M packages/lib-b/package.json
?? CHANGELOG.md
?? packages/lib-a/CHANGELOG.md
?? packages/lib-b/CHANGELOG.md

pnpm larna publishを実行すると、

  • lerna.jsonと各パッケージのバージョンの更新
  • CHANGELOG.mdの作成/更新
  • version/CHANGELOG.mdの変更 git commit
  • リリースバージョンのタグ付け git tag
  • コミットとタグのpush git push
  • npmレジストリへのpublish

実際の変更内容はこんな感じ。

$ git diff lerna.json
diff --git a/lerna.json b/lerna.json
index 8c45f97..ef95be7 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,6 +1,6 @@
 {
   "$schema": "node_modules/@lerna-lite/cli/schemas/lerna-schema.json",
-  "version": "0.0.0",
+  "version": "0.0.1",
   "packages": [
     "packages/*


$ git diff packages/lib-a/package.json
diff --git a/packages/lib-a/package.json b/packages/lib-a/package.json
index a817e2f..f1e1f28 100644
--- a/packages/lib-a/package.json
+++ b/packages/lib-a/package.json
@@ -1,6 +1,6 @@
 {
   "name": "lib-a",
-  "version": "0.0.0",
+  "version": "0.0.1",
   "description": "",
   "main": "index.js",
   "scripts": {
@@ -13,6 +13,7 @@
   "license": "ISC",
   "devDependencies": {
     "@types/node": "^20.3.3",
-    "lib-b": "workspace:^"
-  }
+    "lib-b": "^0.0.1"
+  },
+  "gitHead": "ee3ab490a4e3bb027ac13aa5d7f518ec3756cac3"
 }
$ cat CHANGELOG.md
# Change Log

All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

## 0.0.1 (2023-07-05)

**Note:** Version bump only for package example_monorepo


$ cat packages/lib-a/CHANGELOG.md
# Change Log

All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

## 0.0.1 (2023-07-05)

**Note:** Version bump only for package lib-a

dry-runの結果を確認したら、元に戻しておく

$ git checkout -- .
$ git clean -f
Removing CHANGELOG.md
Removing packages/lib-a/CHANGELOG.md
Removing packages/lib-b/CHANGELOG.md

最初のリリース

git pushだけしないように設定。

# dry-run
$ pnpm publish:latest --no-push
Changes (2 packages):
 - lib-a: 0.0.0 => 0.0.1
 - lib-b: 0.0.0 => 0.0.1

パッケージをリリースするNPM関連の設定とかをしていないので、
パッケージの公開に関しては以下のようにエラーがでる

lerna-lite WARN notice Package failed to publish: lib-b
lerna-lite ERR! E429 429 Too Many Requests - PUT https://registry.npmjs.org/lib-b
lerna-lite ERR! E429 429 Too Many Requests - PUT https://registry.npmjs.org/lib-b

コミットされた内容とかを確認してみると、
ちゃんとコミットとタグ付けがされている。

$ git log -1
commit 9c5d7018958ebe4248fed4e21cc8368193ea37bd (HEAD -> main, tag: v0.0.1)
Author: memory-lovers <8280057+memory-lovers@users.noreply.github.com>
Date:   Wed Jul 5 10:00:05 2023 +0900

    v0.0.1

$ git tag
v0.0.1

Conventional Commitsを使ったのbump

lerna publish --yes --conventional-commitsのように、
--conventional-commitsをつけていると、
コミットログからどのバージョンを上げるかを自動で決めてくれる。

インクリメントされる箇所は、以下のコミットのtypeに従う。

  • PATCH ... fix:
  • MAINOR ... feat:
  • MAJOR ... fix!: / feat!
  • no bump ... build: / chore: / ci: / docs: / style: / refactor: / perf: / test:

Conventional Commits

カナリアリリース

何も変更がないとこんな感じで、何もおきない。

$ pnpm publish:canary --no-push
lerna-lite notice cli v2.4.2
lerna-lite info canary enabled
lerna-lite notice Current HEAD is already released, skipping change detection.
lerna-lite success No changed packages to publish 

なので、改行を追加するだけのコミットを追加。

$ echo "" >> packages/lib-a/package.json
$ git commit -am "update"

もう一度。

$ pnpm publish:canary --no-push
$ git st -s
 M packages/lib-a/package.json
 
$ git diff 
diff --git a/packages/lib-a/package.json b/packages/lib-a/package.json
index 9b59a83..f118109 100644
--- a/packages/lib-a/package.json
+++ b/packages/lib-a/package.json
@@ -1,6 +1,6 @@
 {
   "name": "lib-a",
-  "version": "0.0.1",
+  "version": "0.0.2-beta.0+d445d82",
   "description": "",
   "main": "index.js",
   "scripts": {
@@ -13,7 +13,7 @@
   "license": "ISC",
   "devDependencies": {
     "@types/node": "^20.3.3",
-    "lib-b": "workspace:^"
-  }
+    "lib-b": "^0.0.1"
   }
 }
-

こんな感じで、変更のあるパッケージだけ、
バージョンを更新してくれる。

pnpm larna publish --carnaryを実行すると、

  • 各パッケージのバージョンの更新
  • npmレジストリへのpublish

をしてくれて、CHANGELOG.mdなどは更新されない。

バージョンは統一かパッケージ個別か

上記の例では全パッケージ共通のバージョンにしているけど、
個別にバージョンをつけることもできる。

  • 統一モード ... fixed mode
  • 個別モード ... independent mode

please make sure that you have a lerna.json config file and a version property defined with either a fixed or independent mode (for example: "version": "independent"). An error will be thrown if you're missing any of them.

これに従い、lerna.jsonversionを変更すればOK

  {
    //...
-    "version": "0.0.0",
+    "version": "independent",
    "npmClient": "pnpm",
    "packages": ["packages/*"]
  }

ハマったポイント

--carnaryオプションは、割といろいろハマった。。

  • lerna publish --carnaryはtagがないと全部publish。。
    • --carnaryオプションはタグが1つ以上あるときにフラグが立つよう
  • --dry-runをつけるとエラーになる。。
    • lerna publish --canary --dry-runの問題はv3.2.1で対応されたっぽい!

GitHub Packagesで非公開パッケージを配布

GitHub Packagesを使うと、
非公開のパッケージを配布することができる。

GitHub Packagesを使うためにいくつかルールがあるので、
それに従い、設定していく

  • スコープ付きのnpmパッケージのみ
  • 同じリポジトリから複数パッケージを公開する場合は、
    repositoryを設定する

リポジトリのURLが
https://github.com/memorylovers/example_monorepo
だった場合は、こんな感じ。

package.jsonの設定

$ git diff
diff --git a/package.json b/package.json
 {
-  "name": "example_monorepo",
+  "name": "@memorylovers/example_monorepo",
   "version": "0.0.1",

diff --git a/packages/lib-a/package.json b/packages/lib-a/package.json
 {
-  "name": "lib-a",
+  "name": "@memorylovers/lib-a",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/memorylovers/example_monorepo",
+    "directory": "packages/lib-a"
+  },
   "version": "0.0.1",
 }
-

diff --git a/packages/lib-b/package.json b/packages/lib-b/package.json
 {
-  "name": "lib-b",
+  "name": "@memorylovers/lib-b",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/memorylovers/example_monorepo",
+    "directory": "packages/lib-b"
+  },
   "version": "0.0.1",

.npmrcの設定

@memorylovers/*のパッケージは、GitHub Packagesを見てほしいので、
.npmrc~/.npmrcを作成 or 変更

# ~/.npmrc
; registory
@memorylovers:registry=https://npm.pkg.github.com

; GitHub PAT(classic): Write GitHub Packages
//npm.pkg.github.com/:_authToken=ghp_<YOUR_PERSONAL_ACCESS_TOKEN>

また、パッケージの公開やインストールには、
個人用アクセストークン(PAT)が必要なので、それも合わせて設定。
Personal access tokens (classic)じゃないといけないので注意。

ダウンロードだけなら、read:packages権限でOK。
アップロードもするので、write:packages権限が必要。

これでローカルからpublishできるように。

$ pnpm publish:canary
// ...
Found 2 packages to publish:
 - @memorylovers/lib-a => 0.0.2-beta.1+500dec2
 - @memorylovers/lib-b => 0.0.2-beta.1+500dec2

// ...

Successfully published:
 - @memorylovers/lib-a@0.0.2-beta.1+500dec2
 - @memorylovers/lib-b@0.0.2-beta.1+500dec2

GitHub Actionsで自動デプロイ

ローカルからパッケージを公開できるようになったので、
GitHub Actionsを使って、自動でデプロイできるようにしてみる。

workflowファイルはこんな感じ。

# .github/workflows/release-latest.yml
name: Publish Latest
"on":
  push:
    branches:
      - main
    paths:
      - "**/package.json"
      - "**/pnpm-lock.yaml"
      - "workspaces/**"
  workflow_dispatch:

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      # *****************************************************
      # * SETUP
      # *****************************************************
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - uses: pnpm/action-setup@v2
        with:
          version: 8

      - uses: actions/setup-node@v3
        with:
          cache: "pnpm"
          node-version: "18"
          registry-url: https://npm.pkg.github.com
          scope: "@memorylovers"

      - run: pnpm i

      # *****************************************************
      # * Setup Git Config
      # *****************************************************
      - run: |
          git config --global user.name 'github-actions[bot]'
          git config --global user.email 'github-actions[bot]@users.noreply.github.com'
          git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/$GITHUB_REPOSITORY
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      # *****************************************************
      # * Build
      # *****************************************************
      - run: pnpm build

      # *****************************************************
      # * Publish
      # *****************************************************
      - run: pnpm publish:latest
        env:
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

ポイントとしては以下の3点

  • actions/setup-nodeでregistryなどの設定
  • commit/pushをするのでgit config --globalを追加
  • lerna publishのために、NODE_AUTH_TOKENなどが必要

あとは、git pushして少し待つと、リリースコミットがされてる感じ。

$ git push -u origin main
$ git pull
Updating f12647d..276ecf8
Fast-forward
 CHANGELOG.md                | 4 ++++
 lerna.json                  | 2 +-
 packages/lib-a/CHANGELOG.md | 4 ++++
 packages/lib-a/package.json | 2 +-
 packages/lib-b/CHANGELOG.md | 4 ++++
 packages/lib-b/package.json | 2 +-
 6 files changed, 15 insertions(+), 3 deletions(-)

$ git tag
v0.0.2
v0.0.1

GitHub Packageにあるパッケージを利用する

公開したパッケージを利用したい場合は、PATが必要なので注意。

# ~/.npmrc
; registory
@memorylovers:registry=https://npm.pkg.github.com

; GitHub PAT(classic): Read GitHub Packages
//npm.pkg.github.com/:_authToken=ghp_<YOUR_PERSONAL_ACCESS_TOKEN>

GitHub Actions内でパッケージを利用する

GitHub Actions内でも、PATが必要なので注意。

pnpm installする前に、.npmrcに追記をしておく。

name: Build And Deploy
"on":
  push:
    branches:
      - main
  workflow_dispatch:
jobs:
  build_and_deploy:
    runs-on: ubuntu-latest
    steps:
      # *****************************************************
      # * SETUP
      # *****************************************************
      - uses: actions/checkout@v3
      
      - uses: pnpm/action-setup@v2
        with:
          version: 8

      - uses: actions/setup-node@v3
        with:
          cache: "pnpm"
          node-version: "18"

      - name: setup .npmrc
        run: |
          echo "@memorylovers:registry=https://npm.pkg.github.com" >> .npmrc
          echo "//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_PAT }}" >> .npmrc
          
      # install and build
      - run: pnpm install
      - run: pnpm build

以上!! これでだいぶ共通化が捗りそう。。(´ω`)

参考にしたサイト様

公式サイト