LoginSignup
8
12

More than 3 years have passed since last update.

Yarn workspaces から Lerna に移行した

Last updated at Posted at 2020-04-03

Yarn workspaces から Lerna に移行した時の知見です。
やや書きかけ項目です。

前提

とあるサービスを提供しており、モノレポで運用している。使用スタックは主に API の Ruby on Rails で GraphQL の API サーバーを構築し、そのフロントエンドを React/ReactNative で作成している。

もともとは別々のリポジトリだったが、提供サービスが増えてくるに従い、下記の問題点が出てきた。

  • CI/CD パイプラインを一々作成する必要があり、反映にも手間がかかる
  • GraphQL の型定義やユーティリティ、細かいところでは Git の Hook など、再利用したいものがある
  • GitHub への招待などが手間になる

そこで、この記事を書く半年前くらいにリポジトリを統合し、モノレポになった。

Rails と それ以外 (TypeScript) でフォルダを分割し、 TypeScript 側は手軽にモノレポをマネジメントできそうな Yarn workspaces を選択した。その時はこれで苦しむことを知らない。

当初のディレクトリ構成は、下記。

.
|-- api/                    # Rails API
|-- docker-compose.yml
|-- package.json            # 開発用のパッケージ (prettier など)
|-- packages/               # TypeScript clients
|    |-- app                # アプリ (React Native)
|    |    |-- src/
|    |    `-- package.json
|    |-- console            # 管理画面 (React Native Web)
|    `-- components         # 共有コンポーネント
|-- prettier.config.js
|-- private/                 # AWS のトークンなど、秘匿ファイル。git-crypt で暗号化
|-- tsconfig.json           # 使いまわしている
|-- tslint.json
`-- yarn.lock

まとめると、

  • サーバーサイドは主に Ruby on Rails
  • iOS/Android アプリ 及びそれに付随する Web サービス(管理画面など)を提供している
    • iOS/Android アプリは React Native を使用
    • Web サービスには React Native Web を使用
    • React Native で作成されたコンポーネントを、一部 Web でも使用している

が、上記の構成では限界を感じたため、 Lerna に移行することにした。

Lerna 移行の動機

React Native で Yarn workspaces は、 Yarn の hoist (巻き上げ)機能により、非常に使いにくいことが分かってきた。

致命的なのは、 react-native-cli は、 React Native における index.jsAppRegistry.registerComponent する場所) の 相対パス./node_modules/react-native/cli.js を見に行くため、巻き上げた先のパスを設定する必要がある。

巻き上げられると、 Root の node_modules を見に行くため、ビルド時に ../../node_modules/react-native/cli.js に設定する必要がある。つまり、 Xcode をいじる必要が出てくる。

また、 Root から実行する際は下記のスクリプトが必要になってくる。

// @see https://github.com/facebook/react-native/issues/25822#issuecomment-531009417

process.chdir('./packages/nupp1-fit')

var cli = require('@react-native-community/cli')
cli.run()

ここまでは、まだ設定すれば問題無いが、 場合によって 巻き上げられないパッケージがあるので、他のパッケージを更新した際に、 React Native がビルドされるかを確認する必要がある。

特に React Native 0.60.0 からの autolink 機能は、 Cocoapod でインストールした時に、暗黙的に ./node_modules 以下を探索するので、../../node_modules に巻き上げられると使えなくなってしまう。

では Yarn の nohoist 機能を使えばよいではないか、という意見がありそうだが、それもうまくいくとは限らない。調べた限りでは、下記のような動作をする。

  • nohoist 機能は、動作しないことがある。
  • 何が巻き上げられるかが不明瞭。多分ロジックを追えば分かるが、各ライブラリの依存関係を調べる必要があり、果てしない。
  • 同一のパッケージ/バージョンがあると巻き上げられるらしい。
    • バージョンが異なる場合、各パッケージの node_modules に保存される。
  • キャッシュが効かないことがある → CIの低速化
  • Symbolic link (.bin/**) が壊れることがある。 (semver など)
    • 多分、多数のパッケージが依存しているパッケージで、バージョンによって bin が違う場合、発生する。
  • 各パッケージでインストールした後、 Root でインストールすると、依存関係を変更してないのに、再度インストールが走る。

この問題を調査していたところ、 Lerna はデフォルトで巻き上げをしない(正確にはするが、 Root の package.json にある場合のみ)ので、これを使って解決できそうな気がした。

実作業・困ったこと

基本的には、こちらのリポジトリを参考にしながら、作業を進めていった。

コマンドが複雑化する

Lerna を入れて、ローカルのパッケージ同士を package.json に記述して依存させると、 yarn add 等は 一切できなくなる

また cross-env や webpack-dev-server など、開発時のみ必要になる一部の devDependencies は Root の package.json のみに記述していたので(これは要改善)、各パッケージ配下では実行できない。

毎回 yarn lerna exec --ignore @my/types --scope @my/package tsc などするのも面倒だったので、 Makefile を作成し、その中に npm scripts に相当するスクリプトを記述することにした。

抜粋するとこんな感じ。

SHELL := /bin/bash
LERNA_OPTION := --stream --parallel --no-bail --ignore @my/types --ignore @my/js-project
NODE_BIN := ./node_modules/.bin

export PATH := $(NODE_BIN):$(PATH)

tsc:                                           # Execute `tsc` in each scripts
    lerna exec $(LERNA_OPTION) tsc

参考: https://qiita.com/Hoishin/items/0e9b4ebee45e3f8cdc29

React Native CLI

React Natvie CLI に Lerna が生成する Symbolic Link を追わせるためには、 metro.config.js を少々いじる必要がある。
React Native 0.62.0-RC3 で利用するときは、下記のような設定が必要になる。

resolver.watchFoldersextraNodeModules の設定がポイント。

参考

const path = require('path')
const { getDefaultConfig } = require('metro-config')

function getProjectModuleDir(m) {
  return path.resolve(__dirname, `node_modules/${m}`)
}

// To allow importing peerDependency from other packages
const modulesResolvedInProject = [
  '@babel/runtime',
  '@react-native-firebase/app',
  '@react-native-firebase/analytics',
  '@react-native-community/push-notification-ios',
  '@react-native-community/async-storage',
  'react',
  'react-is',
  'react-native',
  'react-native-appsflyer',
  'react-native-device-info',
  'react-native-picker',
  'react-native-keyboard-aware-scroll-view',
  'react-native-google-places-autocomplete',
  'react-native-keyboard-spacer',
  'react-native-linear-gradient',
  'react-native-paper',
  'react-native-svg',
  'react-native-vector-icons',
  'react-redux',
  'react-native-repro',
]

const extraNodeModules = modulesResolvedInProject.reduce((acc, m) => {
  return { ...acc, [m]: path.resolve(__dirname, `node_modules/${m}`) }
}, {})

module.exports = (async () => {
  const {
    resolver: { sourceExts, assetExts },
  } = await getDefaultConfig()
  return {
    transformer: {
      babelTransformerPath: require.resolve('react-native-svg-transformer'),
    },
    watchFolders: [
      // To allow finding files outside mobile
      path.resolve(__dirname, '..'),
    ],
    resolver: {
      assetExts: assetExts.filter(ext => ext !== 'svg'),
      sourceExts: [...sourceExts, 'svg'],
      extraNodeModules,
    },
    maxWorkers: 2,
  }
})()

総評

良かったこと

  • きちんと依存関係を各リポジトリの package.json に記述しないと、 Symlink が設定されない。
    • Yarn workspaces は何となくでも簡単に動いてしまったので、これは逆に良かった。
  • Lerna bootstrap の方が yarn install より速い(体感)
  • 今後、 Docker Image の作成をする時に、苦労しなそう
    • 依存関係は、各パッケージでちゃんとインストールしないといけないため
  • 各パッケージ以下でコマンドを並列実行できるようになった
    • それ以前は、 wsrun というものを使っていた

手間取ったこと

  • セットアップ手順が複雑化。 yarn install && yarn lerna bootstrap && yarn postinstall
    • 上記の Makefile で記述したため、解決
  • CI の書き換えなど
8
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
12