Nxで非Javascriptプロジェクトのモノレポを構築する

> この記事は Applibot Advent Calendar 2024 22日目の記事です。前回の記事は [コチラ](https://zenn.dev/shochan/articles/0de91b5b2e52c3) です。

# 本記事の目的
Nx を用いて非 Javascript プロジェクトの[モノレポ](https://monorepo.tools/#what-is-a-monorepo)を構築する際に、公式ドキュメントにはチュートリアルなどでまとまっておらず少し困ったので、自分がドキュメントを読みながら試行錯誤して行った手順をまとめる。

# 構築するもの
- 複数の `go.mod` をもつ Go のモノレポに Nx を導入する
- Go 以外の言語を増やした場合でも使える手順・ディレクトリ構成にする
- main ブランチとの差分を検知して、変更があるモジュールのみフォーマット・リント・テスト・ビルドできるようにする
- どのようにフォーマットやリントするかなどを柔軟に設定できる

<details><summary>最終ディレクトリ構成</summary>

```
.
├── .gitignore
├── .nx
│   ├── cache
│   │   ├── run.json
│   │   └── terminalOutputs
│   ├── installation
│   │   ├── node_modules
│   │   ├── package-lock.json
│   │   └── package.json
│   ├── nxw.js
│   └── workspace-data
├── Makefile
├── go
│   ├── .golangci.yaml
│   ├── app
│   │   └── bff
│   │       ├── Makefile
│   │       ├── cmd
│   │       │   └── api
│   │       │       └── main.go
│   │       ├── go.mod
│   │       └── project.json
│   ├── pkg
│   │   └── logger
│   │       ├── Makefile
│   │       ├── go.mod
│   │       ├── go.sum
│   │       ├── logger.go
│   │       └── project.json
│   └── tools
├── nx
├── nx.bat
└── nx.json
```
</details>

#### 前提
- デバイスは Macbook
- Node.js がインストールされている
- Go がインストールされている
- Github をレポジトリに使っている（手順3）

<details><summary>初期ディレクトリ構成</summary>

```
.
└── go
    ├── .golangci.yaml
    ├── app
    │   └── bff
    │       ├── cmd
    │       │   └── api
    │       │       └── main.go
    │       └── go.mod
    ├── pkg
    │   └── logger
    │       ├── go.mod
    │       ├── go.sum
    │       └── logger.go
    └── tools

```
</details>

#### 補足

なお、サードパーティのプラグインとして [@nx-go/nx-go](https://github.com/nx-go/nx-go) が存在するが、フォーマットに [goimports](https://pkg.go.dev/golang.org/x/tools/cmd/goimports) を使ったり、リントに [golangci-lint](https://github.com/golangci/golangci-lint) を使ったりすることができなさそうだったため、今回は使わないものとする。

# 手順
### 1. Nx のセットアップ
公式ドキュメントの「[Install Nx in a Non-Javascript Repo](https://nx.dev/getting-started/installation#install-nx-in-a-nonjavascript-repo)」のように init コマンドをルートディレクトリで実行する。（要 [Node.js](https://nodejs.org/en)）
```shell
$ npx nx init
Setting Nx up installation in `.nx`. You can run Nx commands like: `./nx --help`
CREATE nx.json
CREATE .gitignore
CREATE .nx/nxw.js
CREATE nx.bat
CREATE nx
```

生成される `nx` ファイルがコマンドを実行するためのファイル、`nx.json` が設定ファイル、`.nx/` 配下にプラグインやキャッシュなどが格納される。
`nx.json` に下記の修正をいれる。（JSON Schema の設定と、変更差分を検知するためのベースブランチ設定）
```diff:nx.json
 {
+  "$schema": ".nx/installation/node_modules/nx/schemas/nx-schema.json",
   "installation": {
     "version": "20.3.0"
-  }
+  },
+  "defaultBase": "main"
 }
```

### 2. 既存の Go のモジュールを Nx プロジェクト化する
`go.mod` があるディレクトリに、`project.json` を作成する。（下記は `go/app/bff` の例）
`project.json` を作成することで、Nx にプロジェクトとして認識されるようになる。

```json:go/app/bff/project.json
{
  "name": "go/app/bff",
  "$schema": "../../../.nx/installation/node_modules/nx/schemas/project-schema.json",
  "projectType": "application",
  "sourceRoot": "go/app/bff",
  "tags": [],
  "targets": {
    "build": {
      "command": "go build -o bin/bff-api ./cmd/api",
      "options": {
        "cwd": "go/app/bff"
      }
    },
    "serve": {
      "command": "go run cmd/api/main.go",
      "options": {
        "cwd": "go/app/bff"
      }
    },
    "fmt": {
      "command": "go fmt ./...",
      "options": {
        "cwd": "go/app/bff"
      }
    },
    "lint": {
      "command": "go vet ./...",
      "options": {
        "cwd": "go/app/bff"
      }
    },
    "test": {
      "command": "go test -v ./...",
      "options": {
        "cwd": "go/app/bff"
      }
    }
  }
}
```

上記の `targets` に列挙されているものがそれぞれ [Nx タスク](https://nx.dev/features/run-tasks)の識別子となる。

JSON Schema を参照し、`go/pkg/logger`のものも作成する。（上記例で、`go/app/bff` を `go/pkg/logger` に置き換え、`projectType` を `library` にする）

#### タスク実行方法
例えばルートディレクトリで下記コマンドを実行することで、指定プロジェクトの test タスクを実行できる。
```shell
$ ./nx test go/app/bff
```

全てのプロジェクトの指定タスクを実行する場合は、下記コマンドとなる。
```shell
$ ./nx run-many -t test
```

main ブランチとの変更差分があるプロジェクトのみで指定タスクを実行する場合は、下記コマンドとなる。
```shell
$ ./nx affected -t test
```

また、エディタ拡張の [Nx Console](https://nx.dev/getting-started/editor-setup) を使うと以下のように GUI で各コマンドを実行できるようになる。（下記は Jetbrains の例）
![Screenshot 2024-12-22 at 10.09.01 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496539/9bb8ee1b-5152-3eb9-2f09-77d95d3091bb.png)

### 3. コマンドをカスタマイズする（こちらは一例）
今回は、コマンドのカスタマイズとして下記を一元的に記述して実行するために `Makefile` を用いる。

- fmt は、[goimports](https://pkg.go.dev/golang.org/x/tools/cmd/goimports)・[gofumpt](https://github.com/mvdan/gofumpt)・go mod tidy が全て実行されるようにする
- lint は、 [golangci-lint](https://github.com/golangci/golangci-lint) を使用し、設定ファイルは共通のものを使う。また、[govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck) も実行する
- Go のプライベートレポジトリ参照も考慮したコマンドを実行できるようにする

プロジェクトのルートディレクトリに下記 `Makefile` を作成する。
`<your-github-org>`と`<your-github-repository>`は適切に変えてください。

```makefile:Makefile
### General
SHELL := /bin/bash
ROOT := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
BIN := $(ROOT)/bin

### GitHub
GITHUB_ORG := <your-github-org>
REPOSITORY_NAME := <your-github-repository>

### Go
GO ?= go
GO_ENV ?= CGO_ENABLED=0 GOPRIVATE=github.com/${GITHUB_ORG} GOBIN=$(BIN)
GO_MODULE ?= github.com/${GITHUB_ORG}/${REPOSITORY_NAME}

### Tools
$(shell mkdir -p $(BIN))

GOIMPORTS_VERSION := 0.28.0
$(BIN)/goimports-$(GOIMPORTS_VERSION):
	unlink $(BIN)/goimports || true
	$(GO_ENV) ${GO} install golang.org/x/tools/cmd/goimports@v$(GOIMPORTS_VERSION)
	mv $(BIN)/goimports $(BIN)/goimports-$(GOIMPORTS_VERSION)
	ln -s $(BIN)/goimports-$(GOIMPORTS_VERSION) $(BIN)/goimports

GOFUMPT_VERSION := 0.7.0
$(BIN)/gofumpt-$(GOFUMPT_VERSION):
	unlink $(BIN)/gofumpt || true
	$(GO_ENV) ${GO} install mvdan.cc/gofumpt@v$(GOFUMPT_VERSION)
	mv $(BIN)/gofumpt $(BIN)/gofumpt-$(GOFUMPT_VERSION)
	ln -s $(BIN)/gofumpt-$(GOFUMPT_VERSION) $(BIN)/gofumpt

GOLANGCI_LINT_VERSION := 1.62.2
$(BIN)/golangci-lint-$(GOLANGCI_LINT_VERSION):
	unlink $(BIN)/golangci-lint || true
	$(GO_ENV) ${GO} install github.com/golangci/golangci-lint/cmd/golangci-lint@v$(GOLANGCI_LINT_VERSION)
	mv $(BIN)/golangci-lint $(BIN)/golangci-lint-$(GOLANGCI_LINT_VERSION)
	ln -s $(BIN)/golangci-lint-$(GOLANGCI_LINT_VERSION) $(BIN)/golangci-lint

GOVULNCHECK_VERSION := 1.1.3
$(BIN)/govulncheck-$(GOVULNCHECK_VERSION):
	unlink $(BIN)/govulncheck || true
	$(GO_ENV) ${GO} install golang.org/x/vuln/cmd/govulncheck@v$(GOVULNCHECK_VERSION)
	mv $(BIN)/govulncheck $(BIN)/govulncheck-$(GOVULNCHECK_VERSION)
	ln -s $(BIN)/govulncheck-$(GOVULNCHECK_VERSION) $(BIN)/govulncheck


### Commands

.PHONY: initialize
initialize: initialize-once initialize-once-go

.PHONY: initialize-once
initialize-once:
	git config --global url.git@github.com:${GITHUB_ORG}/.insteadOf https://github.com${GITHUB_ORG}/ # NOTE: go でマルチモジュール構成を取っており、プライベートレポジトリを参照するための設定

.PHONY: initialize-once-go
initialize-once-go: $(BIN)/goimports-$(GOIMPORTS_VERSION) $(BIN)/gofumpt-$(GOFUMPT_VERSION) $(BIN)/golangci-lint-$(GOLANGCI_LINT_VERSION) $(BIN)/govulncheck-$(GOVULNCHECK_VERSION)

### Development Commands
.PHONY: fmt
fmt:
	./nx affected -t fmt

.PHONY: lint
lint:
	./nx affected -t lint

.PHONY: test
test:
	./nx affected -t test

.PHONY: graph
graph:
	./nx graph

### Go Commands

.PHONY: fmt-go
fmt-go: $(BIN)/goimports-$(GOIMPORTS_VERSION) $(BIN)/gofumpt-$(GOFUMPT_VERSION)
	$(GO_ENV) $(GO) mod tidy
	$(BIN)/goimports -local $(GO_MODULE)  -w ./
	$(BIN)/gofumpt -l -w ./

.PHONY: lint-go
lint-go: $(BIN)/golangci-lint-$(GOLANGCI_LINT_VERSION) $(BIN)/govulncheck-$(GOVULNCHECK_VERSION)
	$(BIN)/golangci-lint run ./... -v -c $(ROOT)/go/.golangci.yaml ./...
	$(BIN)/govulncheck ./...

.PHONY: test-go
test-go:
	$(GO_ENV) $(GO) test -v ./...
```

`Makefile` により、make コマンドでカスタマイズしたフォーマット・リント・テストが実行できるようになる。

上記 `Makefile` を include した `Makefile` を各プロジェクトに作成し、`project.json`のコマンドを下記のように修正する。
```makefile:go/app/bff/Makefile
include ../../../Makefile
```

```diff:go/app/bff/project.json
diff --git a/go/app/bff/project.json b/go/app/bff/project.json
index 7a9d997..a84c147 100644
--- a/go/app/bff/project.json
+++ b/go/app/bff/project.json
@@ -18,19 +18,19 @@
       }
     },
     "fmt": {
-      "command": "go fmt ./...",
+      "command": "make fmt-go",
       "options": {
         "cwd": "go/app/bff"
       }
     },
     "lint": {
-      "command": "go vet ./...",
+      "command": "make lint-go",
       "options": {
         "cwd": "go/app/bff"
       }
     },
     "test": {
-      "command": "go test -v ./...",
+      "command": "make test-go",
       "options": {
         "cwd": "go/app/bff"
       }

```

`go/pkg/logger`も同様のファイル作成と変更を加える。


# おわりに
以上で、`Makefile` によりどのようにフォーマットやリントするかなどを柔軟に設定できる Nx を用いた Go のモノレポを作成することができる。

ルートディレクトリで、`make fmt` などと実行することで main ブランチとの差分を検知して、変更があるモジュールのみにフォーマットなどをかけることができる。

2 の手順までは他言語でも同様であり、コマンドをカスタマイズすることで柔軟にフォーマットなどをカスタマイズすることができる。


ネクストアクションとして、キャッシュの最適化に関するドキュメントを記載しておく。
- https://nx.dev/features/cache-task-results


# 参考
- [`Nx`の公式サイト](https://nx.dev/)