AngularのextractLicensesみたいなやつをVite + Reactでもやりたい


## 概要

Vite + Reactで開発をしている時にOSSのライセンスを出す必要があり、AngularのextractLicensesみたいに出せたらいいなと思いました。

## AngularのextractLicensesとは

Angular CLIのビルドオプションで、Webバンドルと一緒に依存パッケージのライセンス情報(`3rdpartylicenses.txt`)を出力してくれます。ビルド時に `ng build --extract-licenses=true` と指定したりangular.jsonで設定したりもできますが、production buildではデフォルトで有効になっています。
生成したライセンス情報をHTTPリクエストでバンドルから取得し、UI上に表示することができます。

https://angular.dev/cli/build#Options

以下のように実装することでコンポーネント上にライセンスを表示することができます。

```app/license/license.component.ts
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { Component, OnInit, inject } from '@angular/core';
import { firstValueFrom } from 'rxjs';

@Component({
  selector: 'app-license',
  standalone: true,
  imports: [HttpClientModule],
  styleUrl: './license.component.scss',
  template: `
    @if (licenseText !== undefined) {
      <p style="white-space: pre-wrap">
        {{ licenseText }}
      </p>
    } @else {
      <div>Loading...</div>
    }
  `,
})
export class LicenseComponent implements OnInit {
  licenseText: string | undefined;
  private readonly http = inject(HttpClient);

  ngOnInit(): void {
    this.getLicenseText();
  }

  private async getLicenseText(): Promise<void> {
    // development buildで見るためにassetsにコピーしました(ごめんなさい)
    this.licenseText = await firstValueFrom(
      this.http.get('assets/3rdpartylicenses.txt', { responseType: 'text' })
    );
  }
}

```

![Screenshot 2023-12-04 at 21.56.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3529817/3fc21f2a-727c-0882-4631-63004c46b48a.png)

## Vite + Reactでやる

### 1. やり方を決める

ササッと確認したかった + Viteのビルド周りに手を入れたくなかったので以下の方法にしました。

1. ビルド前にライセンス情報を `public/` に出力する。
2. `vite build` で `public/` の中のライセンス情報を `dist/` に内包する。
3. アプリケーションからHTTPリクエストで内容を取得する。

```md
vite-react-project
├── public // <- ライセンスをここに出力したい
├── src
│ ├── assets
│ ├── App.css
│ ├── App.tsx
│ ├── index.css
│ ├── main.tsx
│ └── vite-env.d.ts
├── index.html
├── package.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
```

### 2. ライセンス情報の出力

欲しいのは [license-extractor.ts](https://github.com/angular/angular-cli/blob/main/packages/angular/build/src/tools/esbuild/license-extractor.ts) のような機能です。  
`package.json -> node_modules -> LICENSE...` の内容をテキストファイルに出すスクリプトを用意するのが面倒だったので、今回はlicense-checkerというnpmライブラリを使用しています。

https://github.com/davglass/license-checker

### 3. ライブラリのインストールと諸々の準備

Vite + Reactのプロジェクトにlicense-checkerをインストールします

```zsh
% npm i -D license-checker
```

ライセンス情報のフォーマットファイルを作成し、出力用のscriptを定義します。license-checkerはJSONで出力できるので `3rdpartylicenses.json` というファイル名にします。

```custom-format.json
{
  "name": "",
  "licenses": "",
  "repository": "",
  "publisher": "",
  "version": "",
  "description": "",
  "copyright": "",
  "licenseText": "",
  "licenseModified": "no",
  "licenseFile": false,
  "path": false
}
```

```package.json
"scripts": {
  "dev": "vite",
  "build": "tsc && vite build",
  "preview": "vite preview",
  "extract-licenses": "license-checker --customPath ./custom-format.json --production --json --out ./public/3rdpartylicenses.json"
}
```

実際にビルドしてバンドルの中身を確認します。

```zsh
% npm run extract-licenses && npm run build
```

`3rdpartylicenses.json` が含まれていますね。

![Screenshot 2023-12-04 at 22.55.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3529817/a402629e-109a-3020-2e28-2297f5be3603.png)

### 4. HTTPリクエストで取得した `3rdpartylicenses.json` の内容を表示する

実際にHTTPリクエストでJSONの中身を取得し、アプリ上で表示します。useEffect + fetchのやっつけ実装ですが、適宜データフェッチライブラリを使うなどして下さい。

```App.tsx
import { useEffect, useState } from "react";

interface License {
  name: string;
  licenses: string;
  repository: string;
  publisher: string;
  version: string;
  description: string;
  copyright: string;
  licenseText: string;
  licenseModified: string;
}

function App() {
  const [licenses, setLicenses] = useState<License[]>();

  useEffect(() => {
    fetch("/3rdpartylicenses.json")
      .then((res) => res.json())
      .then((data) =>
        setLicenses(
          Object.keys(data as Record<string, License>).map((name) => data[name])
        )
      );
  }, []);

  return (
    <>
      <h1>3rd Party Licenses</h1>
      {licenses !== undefined &&
        licenses.map(
          (
            {
              name,
              licenses,
              repository,
              publisher,
              version,
              description,
              copyright,
              licenseText,
              licenseModified,
            },
            i
          ) => (
            <div key={i} style={{ whiteSpace: "pre-wrap" }}>
              {[
                name,
                licenses,
                `repository: ${repository}`,
                `publisher: ${publisher}`,
                `version: ${version}`,
                `description: ${description}`,
                `copyright: ${copyright}`,
                licenseText,
                `licenseModified: ${licenseModified}`,
                "------------------------------------------------------------------------------------------",
              ].join("\n")}
            </div>
          )
        )}
    </>
  );
}

export default App;

```

こんな感じで表示されます。

![Screenshot 2023-12-04 at 23.51.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3529817/4de2697b-0707-9ff8-86b5-42f56cd9cf65.png)

## 参考

https://angular.dev/cli/build#Options

https://qiita.com/kyosho/items/4d321dc5bf3593a0fd1c

https://github.com/davglass/license-checker