every Tech Blog

株式会社エブリーのTech Blogです。

Nxを使ってnpm projectをmonorepo管理した話

DELISH KITCHEN RS事業部では、小売向けにサイネージやチラシ等のサービスを提供しています。 従来は、そのサービスの管理が出来るWebアプリのみ運用していたのですが、新たに広告配信設定用のWebアプリが必要になりました。 そこでNxを使って、2つのアプリをmonorepoで管理し、コードの共通化を計りました。

Nxとは

Nxはmonorepo用の拡張可能な開発ツールセットです。堅牢なCLI、キャッシュシステム、依存性管理などを提供すると共に、Jest、Cypress、ESLint、Prettierなどのモダンなライブラリの統合をサポートしています。元GoogleのAngularチームにいたメンバーによって創設されたNrwlが開発しており、Googleは全てのプロジェクトをmonorepoで管理しているという有名な話がありますが(詳細は知りません)、それと似た開発体験を提供することを目的に開発されているそうです。

Nxへの移行

RS事業部で開発しているWebアプリはAngularで作られており、それをまずNxに移行しました。

f:id:yukiya-takagi:20210128142040p:plain
従来のディレクトリ構成

NxはAngularをサポートしているので、移行自体は簡単でした。 まずNxで新しくworkspaceを作成します。

npx create-nx-workspace --preset=angular

その後、既存のアプリのコードをapps以下に配置し、angular.jsonやtsconfig.json、tslint.jsonなどの設定ファイルを修正し、既存のアプリで使用していたサードパーティのライブラリ(dayjsなど)を新しいworkspaceに追加して、移行を完了しました。

f:id:yukiya-takagi:20210128142201p:plain
現在のディレクトリ構成

※ 現在のcreate-nx-workspaceは、テストフレームワークにデフォルトでJestとCypressが選択されており、AngularデフォルトのKarma、Protractorを使用したい場合は、別途以下のコマンドでアプリを作成する必要があります。

nx generate @nrwl/angular:app myapp --unit-test-runner=karma --e2e-test-runner=protractor`

ライブラリの作成

複数のアプリから共通のコードを使用するために、ライブラリを作成します。

nx generate @nrwl/angular:lib shared

上記のコマンドでlibs配下にsharedディレクトリが作成されます。

今回は例としてSampleComponentをライブラリに作成します。

nx generate component sample --project=shared

作成したら、shared.module.tsのexportsにSampleComponentを追加します。 次に、アプリからSampleComponentを使用するために、tsconfigにパスを追加します。

{
  ...
  "compilerOptions": {
    ...
    "paths": {
      "@lib/shared": ["libs/shared/src/index.ts"],
      "@lib/shared/*": ["libs/shared/src/lib/*"]
    },
    ...
  },
  ...
}

あとは使用したいモジュールでimportすると、使用可能になります。

import { SharedModule } from '@lib/shared';

@NgModule({
  imports: [SharedModule],
  bootstrap: [AppComponent]
})
export class AppModule {}

また、直接SampleComponentをimportしたい場合は、以下のコードで可能です。

import { SampleComponent } from '@lib/shared/sample/sample.component.ts'

CSSの共通化

上記で作成したsharedライブラリに共通のCSSも置いて、使えるようにします。 場所はどこでもいいのですが、私はlibs/shared/src/stylesにファイルを配置しています。

html,
body {
  height: 100%;
}

body {
  margin: 0;
  font-family: 'Helvetica Neue', Arial, 'Hiragino Kaku Gothic ProN', 'Hiragino Sans', Meiryo, sans-serif;
}

そしたら、angular.jsonの、このスタイルを適用したいアプリの箇所に以下を追加します。

"app": {
  "projectType": "application",
  ...
  "architect": {
    "build": {
      ...
      "options": {
        ...
        "styles": [
          "libs/shared/src/styles/styles.scss", // この部分
        ],
        ...
      }
    }
  }
}

また、partialファイル(_mixin.scssなど)をsharedに置いて参照することも可能です。 これも好きな場所にファイルを配置して、angular.jsonでパスを指定するだけです。

 "app": {
  "projectType": "application",
  ...
  "architect": {
    "build": {
      ...
      "options": {
        ...
        "stylePreprocessorOptions": {
          "includePaths": ["libs/shared/src/styles/partials"], // この部分
        },
        ...
      }
    }
  }
}

ここで注意なのが、指定するのはファイルパスではなくディレクトリパスということです。こうしておくことで、partials以下にあるファイル(_mixin.scss)を@import 'mixin'という形で使うことができます。

CI/CD

monorepoにするとCI/CD周りも変わってきます。おそらく多くの人が、変更があったアプリ、またはライブラリだけをテスト、デプロイしたいと考えると思います。Nxはその希望を叶えてくれます。Nxにはaffectedコマンドがあり、変更の影響があるプロジェクトのみに対してテスト、ビルドを実行する機能があります。

CI/CDはGithub Actionsで行っていて、実際のワークフローを例に紹介したいと思います。Github ActionsはPull Requestを作った時とdevelopmasterブランチにマージされたタイミングで走るように設定しています。

例えば、Pull Requestを作った時に走らせるビルドは以下のように設定しています。

name: build
on: pull_request
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v1
        with:
          node-version: '12.x'
      - name: Run cache/restore node_modules
        uses: actions/cache@v1
        with:
          path: node_modules
          key: v1-${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
      - name: Run build
        run: make affected-build BASE=${{ github.event.pull_request.base.sha }} // nx affected:build --base=${BASE} を呼んでるだけ

ここで、affectedはbase optionでブランチやコミットIDを指定でき、baseとHEADの差分から、影響のあるプロジェクトを判断します。その仕様から、actions/checkout@v2ではfetch-depth: 0を指定することで、コミット履歴を全部取得するようにしています。

またmasterにマージされた際のデプロイは以下のように設定しています。

name: deploy
on:
  push:
    branches:
      - master
jobs:
  build:
    ...

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Run cache/restore dist
        uses: actions/cache@v1
        with:
          path: dist
          key: v1-${{ runner.os }}-dist-${{ github.run_number }}
      - name: Run deploy
        run: |
          app_paths="dist/apps/*/"
          if ! ls -d $app_paths &>/dev/null ; then
            exit 0
          fi
          for app_path in $app_paths; do
            app=$(basename "$app_path")
            if [ "$app" == "appName1" ]; then
            elif [ "$app" == "appName2" ]; then
            fi
          done

差分があるもののみデプロイするという仕組みはNx自体にはないので、buildフェーズで生成されたものをデプロイするというスクリプトを書いて対応しています。

現在のデプロイの仕組みだと、masterに入ったものは全部対象になってしまうので、リリース前に全プロジェクトの確認が必要になってしまいます。またmasterに入ってもリリースのタイミングは調整したいこともあるでしょう。monorepoにして一番の課題で、依然他に良いフローがないか検討中の部分です。

まとめ

今回はNxを使ってnpm projectをmonorepo管理した話をしました。Nxを使ったComponentやCSSの共通化、CI/CDの運用とかは普段聞く機会がないので、誰かの参考になれば幸いです。