「サーバーとクライアントの共有部分をライブラリ化したい → でも非公開にしたい」という要望への解決方法の一つ。コードが 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
複数パッケージを一つのリポジトリで管理運用するためのツール。
コミットを元に、サブディレクトリ毎に package
として publish
するなど。
パッケージの依存関係を一括で解決する lerna bootstrap
の機能については、Yarn Workspaces を利用することにした。
Yarn 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
して、そこのフォルダで作業する。いつも通り
git clone https://github.com/GITHUB_USERNAME/GITHUB_REPONAME
cd GITHUB_REPONAME
Yarn Workspaces
のために packages.json
の設定
"private": true
の設定と、"workspaces"
以下の設定を追加。
{
// 略
"private": true,
"workspaces": {
"packages": [
"packages/*"
]
},
lerna
のインストール
インストール
lerna 公式のドキュメントには、いきなり npx lerna init
せよと書かれているが、手元の環境では graceful-fs
関係のエラー(cb.apply is not a function
)が出て動作しなかったので、yarn global add lerna
してから使い始めた。
yarn global add lerna
yarn lerna init
(追記)npx で 「cb.apply is not a function」 エラーが出る を参考に、npx 内での graceful-fs の polyfill を無効化したら通るかも知れない
lerna.json
の設定
設定内容の概略は以下の通りと認識している
- 習わしに倣って
packages/
以下にソースコードを置くことにしたので、"packages": ["packages/*"]
-
yarn
を使うので"npmClient": "yarn"
-
yarn workspaces
を使うので"useWorkspaces": true
- パッケージ毎にバージョンを設定したいので
"version": "independent"
-
LernaとYarn WorkspacesでMonorepo管理 に倣って command を追加したが、まだ十分に理解していない。
lerna publish
でmain
ブランチをGitHub Packages
にpublish
する。その際にはconventionalCommits
でコミットログが書かれているものとして扱うというそのままの認識で良いと思う。
{
"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
を利用した。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
にしておくと良いかも。
{
// 前略
"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
はお手軽そうだった。
個別に入れたい場合は、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
を作成する。ユーザ名とシークレットは置き換えること。
@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
に追加するのをお忘れ無く。
.npmrc
libs
を作成する
npm
パッケージを作る要領で、libs
以下にコードを書いていく。
共通設定はルートの tsconfig.common.json
に記述して、lib
用の設定から読み込んだ。tsconfig.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
に依存関係を指定する。その際にパッケージ化されているバージョン名を指定しないと駄目みたい。
{
// 略
"dependencies": {
"@GITHUB_USERNAME/lib": "^0.6.0",
},
後は、npm
パッケージと同様に import
なり require
なりで利用できるはず。
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
にコピーした。
他には特別な設定は必要なく、firebase deploy --only functions
の実行時間が延びてしまったぐらいで、上手く参照してくれているとおもう。
ライブラリの作り方が不味いなど lerna
と yarn workspaces
には無関係のエラーは起こしていたが、それは別の問題。
新規に git clone した後
yarn
を実行すると、全パッケージの依存関係を全て解決してくれる。node_modules
以下を消した場合なども同様。
npm
を使う場合は、yarn lerna bootstrap
を実行した後、パッケージ毎に npm install
になるっぽい。
参照したページ
感謝。
- Yarn Workspaces 公式
- lerna github
- lerna 公式
- 依存関係の扱い
- LernaとYarn WorkspacesでMonorepo管理
- How to use Firebase Cloud Functions and Yarn Workspaces
- Publishing and Installing Private GitHub Packages using Yarn and Lerna
- Yarn workspaces から Lerna に移行した
- yarn workspaceでmonorepo+TypeScript+Lint環境をつくる
- lernaでのmonorepoにおけるリリースフロー(Fixed/Independent)
- GitHub Packagesで社内用npmパッケージを配布して使ってみた
- Structuring a Firebase web project with Lerna
- モノレポについての誤解 - Misconceptions about Monorepos: Monorepo != Monolith を翻訳しました
- Lernaを使ってFirebase環境のためのモノレポ環境一式をカッコよく構築する
[question] all my package.json's now contain gitHead key. What to do about it?