dependency-cruiserについて理解する

:::note
概要
[dependency-cruiser](https://www.npmjs.com/package/dependency-cruiser?activeTab=readme)は

「循環依存を検知」
「package privateな設計を守る」

このようなことを実現してくれるライブラリです！

スライド版は[こちら](https://speakerdeck.com/yamatai1212/dependency-cruiser-description)です！
:::

# 目次
1. 背景 🖼️
2. 使い方 🚧
    2.1 install
    2.2 configファイル作成
    2.3 依存関係グラフ作成
    2.4 依存関係の検証
3. 実際にコマンド叩いてみる 🧪
    3.1 install
    3.2 configファイル作成
    3.3 依存関係グラフ作成
    3.4 依存関係の検証
4. ユースケース 💡
    4.1 github-actionsで循環依存関係になっているものを検出して知らせる
    4.2 package private のような設計を作る
5. 最後に 🚢
6. 参考 ✨

# 1. 背景 🖼️
https://anotherworks.connpass.com/event/294517/

- こちらのイベントで[dependency-cruiser](https://www.npmjs.com/package/dependency-cruiser?activeTab=readme)という言葉を聞いて気になったからです。

- 業務でTypeScriptのプロジェクトを担当してます。
  exportするとプロジェクト全体にexportされてしまうため、依存関係を保護し続けるのは難しいです。そのため、思いも寄らぬ依存関係が発じたり、循環参照が発生したりする恐れがあります。
  dependency-cuiserはそんな時に有効だと知りました。

# 1. dependency-cruiserとは
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405182/417fd3d3-0a69-feba-5009-c8fae38d9845.png)
https://raw.githubusercontent.com/sverweij/dependency-cruiser/main/doc/assets/sample-dot-output.png

これは、JavaScript、TypeScript、LiveScript、またはCoffeeScriptプロジェクトの依存関係を調べ、次のようにします。
- 自分のルールと照らし合わせて検証する
- テキスト、グラフィックででルール違反を報告する

以下はreactの依存関係グラフです。

https://sverweij.github.io/dependency-cruiser/assets/react-high-level-dependencies.html

# 2. 使い方 🚧

## 2.1 install
```terminal
npm install --save-dev dependency-cruiser
# or
yarn add -D dependency-cruiser
pnpm add -D dependency-cruiser
```

## 2.2 configファイル作成
```terminal
npx depcruise --init
```
環境を少し調べ、いくつか質問をし
プロジェクトに適応した dependency-cruiser.js 設定ファイルを作成するそうです
- 循環する依存関係
- package.jsonで見つからない依存関係
- 孤児
- dev-またはoptionalDependenciesに依存するプロダクションコードの検出
等、ほとんどのプロジェクトで意味のあるルールを追加してくれます

## 2.3 依存関係グラフ作成

graphvizをinstallしてください
```terminal
brew install graphviz
```

例）src ディレクトリ内の依存関係のグラフを作成する
```terminal
npx depcruise src --include-only "^src" --output-type dot | dot -T svg > dependency-graph.svg
```

## 2.4 依存関係の検証
例)src ディレクトリ
```terminal
npx depcruise src
```
ルールと照らし合わせて検証し、違反があればESLintのような形式で表示します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405182/b678acad-f723-db82-6205-af17fd7501d7.png)
https://raw.githubusercontent.com/sverweij/dependency-cruiser/main/doc/assets/sample-err-output.png

# 3. 実際にコマンド叩いてみる 🧪
[sampleリポジトリ](https://github.com/yamatai12/dependency-cruiser-lab)で動かしながら進めていきます。

## 3.1 install
```terminal
# npm i --save-dev dependency-cruiser
```

## 3.2 configファイル作成
```terminal
# npm npx depcruise --init
```
以下のようなことを聞かれました
```terminal
# npx depcruise --init
✔ Where do your source files live? … src
✔ Do your test files live in a separate folder? … no
✔ Looks like you're using a 'tsconfig.json'. Use that? … yes
✔ Full path to your 'tsconfig.json › tsconfig.json
✔ Also regard TypeScript dependencies that exist only before compilation? … yes
✔ Looks like you're using webpack - specify a webpack config? … yes

  ✔ Successfully created '.dependency-cruiser.js'
```
srcディレクトリと同じ階層にconfigファイル（.dependency-cruiser.js）が作成されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405182/63806c0a-4cbf-7d4c-c953-a205b3a1afd5.png)

## 3.3 依存関係グラフ作成

graphvizを使えるようにinstallします。
```dockerfile
FROM node:lts-bullseye
RUN apt-get update && apt-get install -y graphviz
WORKDIR /app
```

docker-compose buildして以下を実行します。

```terminal
# npx depcruise src --output-type dot | dot -T svg > output.svg
```

github上でsvgファイルを見ると以下のようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405182/bbaefddb-18a9-13bc-55d3-afb3d56a411d.png)

## 3.4 依存関係の検証
```terminal
# npx depcruise src

✔ no dependency violations found (2 modules, 1 dependencies cruised)
```
依存関係の違反は見つかりませんでした。
srcディレクトリ配下にはファイルが2つ、依存関係が1つあります。
とのことでした。

### 依存関係の違反を起こしてみます（循環依存）

```index.ts
import { sub } from "./calc";
console.log(sub(1, 1));

export const violation = "violation";
```

```calc.ts
import { violation } from "./index";

export function sub(a: number, b: number) {
  console.log(violation);
  return a - b;
}

```

```terminal
# npx depcruise src

  warn no-circular: src/calc.ts → 
      src/index.ts →
      src/calc.ts

✘ 1 dependency violations (0 errors, 1 warnings). 2 modules, 2 dependencies cruised.
```

依存関係グラフにも循環してるのが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405182/f2e505a9-18ac-6c64-d78b-f8cca8e5b2d1.png)

実際に処理を実行すると、violationの値はundefinedで表示されてます。
```terminal
# node dist/content_scripts.js
undefined
0
```
エラーになるケースや、ならないケースはありますが、意図しない挙動をするということを確認できました。

### 依存関係の違反（循環依存）をwarnからerrorに変更し、validateします。

severity: 'warn',をseverity: 'error'にします。

```.dependency-cruiser.js
/** @type {import('dependency-cruiser').IConfiguration} */
module.exports = {
  forbidden: [
    {
      name: 'no-circular',
      severity: 'error',
      comment:
        'This dependency is part of a circular relationship. You might want to revise ' +
        'your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ',
      from: {},
      to: {
        circular: true
      }
    },
```

この設定でvalidateします。
errorとして表示されました。

```terminal
# npx depcruise src

  error no-circular: src/calc.ts → 
      src/index.ts →
      src/calc.ts

✘ 1 dependency violations (1 errors, 0 warnings). 2 modules, 2 dependencies cruised.
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405182/49415385-f58a-86ea-a3dd-3d186b2d2636.png)
依存関係グラフも赤字で表示されました。

# 4. ユースケース 💡
## 4.1 github-actionsで循環依存関係になっているものを検出して知らせる
```yaml:dependency-test.yml
name: Dependency Test CI

on:
  push:
    branches:
      - '*'
  pull_request:
    branches:
      - '*'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3

    - name: Use Node.js
      uses: actions/setup-node@v3
      with:
        node-version: 18

    - name: Install dependencies
      run: cd app && npm ci

    - name: Run dependency test
      run: cd app && npx depcruise src 
```
以下のようにfailになってくれました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405182/e8121138-95d2-750c-afe5-4615b7baf24c.png)

## 4.2 package private のような設計を作る
内部パッケージのような挙動を実現するルールを作成できます。
- _から始まるディレクトリ内部のファイルは、同ディレクトリ内のファイルまたは、直上のディレクトリのファイルからのみimport可能
- _から始まるファイルは同階層に置かれたファイルからのみimport可能

fromがimport(require)する側で、toがexportする側です。
pathがルールを適用するファイルパスで、pathNotが適用されないファイルパスです。

```.dependency-cruiser.js
module.exports = {
  forbidden: [
    {
      name: `1. '_'から始まるディレクトリ内部のファイルは、同ディレクトリ内のファイルまたは、一つ上の階層のディレクトリのファイルからのみimport可能`,
      severity: 'error',
      from: { path: ['(.*)\\/.*\\.ts'], pathNot: ['.*\\.spec\\.ts$'] },
      to: {
        path: ['_\\w+\\/\\w+\\.ts$'],
        pathNot: ['$1\\/_\\w+\\/\\w+\\.ts$', '$1\\/\\w+\\.ts$'],
      },
    },
    {
      name: `2. '_'から始まるファイルは同階層に置かれたファイルからのみimport可能`,
      severity: 'error',
      from: { path: ['(.*)\\/.*\\.ts$'], pathNot: ['.*\\.spec\\.ts$'] },
      to: {
        path: ['.*\\/_\\w+.ts$'],
        pathNot: ['$1\\/_\\w+.ts$'],
      },
    },
]
```

例で以下のフォルダ構成で解説します。
```
src/
├── _package1/
│   ├── _package2/
│   │   ├── _private.ts 
│   │   └──calc.ts 
│   └── importer.ts
├── index.ts
└── exporter.ts
```

```terminal
# npx depcruise src

  error 2. '_'から始まるファイルは同階層に置かれたファイルからのみimport可能: src/_package1/importer.ts → src/_package1/_package2/_private.ts
  error 1. '_'から始まるディレクトリ内部のファイルは、同ディレクトリ内のファイルまたは、一つ上の階層のディレクトリのファイルからのみimport可能: src/index.ts → src/_package1/_package2/calc.ts

✘ 2 dependency violations (2 errors, 0 warnings). 5 modules, 5 dependencies cruised.
```
以下画像が依存関係グラフで赤字がエラーです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405182/43b0b834-a5ef-5263-13ec-147b8539704f.png)

# 5. 最後に 🚢
- 循環依存関係を自動的に検出してくれるのはありがたいなと思いました！！
  中々原因が掴めず苦労したことが過去ありましたので。。
- private pacakgeな設計ができるので心置きなくモジュール管理できるようになるのも魅力的です。

# 6. 参考 ✨
- [メルカリ Shops の開発を支える Automation 化](https://engineering.mercari.com/blog/entry/20220218-mercari-shops-automation/)
- [ESLintだけでは守れない。Dependency cruiserによるアーキテクチャー保護](https://zenn.dev/hedrall/articles/f565147bee5da8)
- [dependency-cruiser-report-actionでPRの変更ファイルの依存関係を可視化してコメントする](https://zenn.dev/mh4gf/articles/12fcdcba14e576)

# 他にも依存関係の管理に関する記事を書いています 💪

https://qiita.com/yamatai12/items/b91f785ecd3091833c01

https://qiita.com/yamatai12/items/90d61e36ee0013556937

# 参考になる記事を書いてくださった皆様に感謝致します