【2021年4月版】Node.js v15 の npm 7 で TypeScript の MonoRepo に入門

# NPM にもやってきましたワークスペース

[Yarn](https://classic.yarnpkg.com/blog/2017/08/02/introducing-workspaces/)や[Lerna](https://github.com/lerna/lerna)でお馴染みのワークスペース管理ですが、Node v15 の nvm 7 からこれに対応した模様です！

- [Medium: Migrating to a Monorepo Using NPM 7 Workspaces](https://medium.com/edgybees-blog/how-to-move-from-an-existing-repository-to-a-monorepo-using-npm-7-workspaces-27012a100269)

上記の記事では exprimental とありますが、現在では GA の模様です。

https://www.infoq.com/news/2021/02/npm-7-generally-available/

自分も最近では Serverless と SPA での開発がメインでどちらも TypeScript を使うので、Typescript でも宜しく Mono Repository できないものかと考えておりましたら・・・

https://github.com/Quramy/npm-ts-workspaces-example

いい感じの Typescript での Monorepo サンプルを発見いたしましたので、これに沿って Typescript での Monorepo に入門してみたいと思います。

# 1. まず JS から

TypeScript を導入する前に、JS で Workspace の破壊力を試してみましょう。

## 1-1. プロジェクトの階層構造を構築

以下のようなコマンドでプロジェクトのフォルダを作成します。

```bash
$ mkdir project1 && cd $_
$ npm init -y
$ mkdir app
$ mkdir -p lib/component-a
$ mkdir -p lib/component-b
$ cd app && npm init -y
$ cd ../lib/component-a && npm init -y
$ cd ../component-b && npm init -y
$ cd ../..
``` 

結果、以下のようなフォルダ構成のプロジェクトが出来上がります。

```
project1
  ├── app
  │　  └── package.json
  ├── lib
  │　  ├── component-a
  │　  │   └──  package.json
  │　  └── component-b
  │　      └──  package.json
  └──  package.json
```

## 1-2. ワークスペースの宣言

トップの package.json に `workspaces` を追加します。

```json:project1/package.json
{
...
  "workspaces": [
    "libs/*",
    "app"
  ],
...
}
```

上記のような指定で、`app` と `libs` 以下のpackage.jsonを含むサブフォルダ全てを"個別のモジュール"として認識してくれます。
これで、`npm i` をして、結果を表示してみましょう。

```bash
$ pwd
/xxx/xxx/project1
$ npm i
...
$ npm ls
project1@1.0.0 /xxx/xxx/project1
├── app@1.0.0 -> /xxx/xxx/project1/app
├── component-a@1.0.0 -> /xxx/xxx/project1/libs/component-a
└── component-b@1.0.0 -> /xxx/xxx/project1/libs/component-b
```
のような感じで `app` と `component-a`、`component-b`が認識されていることがわかります。

ついでに `node_modules` 内はどうなっているか見てみましょう。

```bash
$ tree
.
├── app
│   └── package.json
├── libs
│   ├── component-a
│   │   └── package.json
│   └── component-b
│       └── package.json
├── node_modules
│   ├── app -> ../app
│   ├── component-a -> ../libs/component-a
│   └── component-b -> ../libs/component-b
├── package.json
└── package-lock.json
```

このような感じで、`app` と `libs` 以下の `component-a`、`component-b` が個別のモジュールとして依存関係に追加されているのが確認できました。

## 1-3. 動作確認用プログラムの追加

プログラム上、ちゃんと認識されているのか、確認してみたいと思います。

シンプルなスクリプトを以下のように作成します。

component-a には以下の index.jsを作成してみます。

```js:libs/component-a/index.js
module.exports = 'A だよ！';
```

続いて component-b には以下の index.jsを作成してみます。

```js:libs/component-b/index.js
module.exports = 'B どす〜';
```

そしてこれらをappから呼び出してみましょう。

```js:app/index.js
console.log("start app");

const moduleA = require('component-a');
console.log(moduleA);

const moduleB = require('component-b');
console.log(moduleB);
```

## 1-4. 動作確認

では、`app/index.js` を起動してみます。

```bash
$ pwd
/xxx/xxx/project1
$ node app/index.js
start app
A だよ！
B どす〜
$ cd app && node index.js
start app
A だよ！
B どす〜
```

トップレベルのディレクトリからも app ディレクトリからでも相対パスなどなしでちゃんと `component-a`と`component-b`が読み込めています。
app/package.json の Dependencies には `component-a`や`component-b`は追加してませんよ！？
さらに`libs`以下の js を書き換えても即座に反映されます。(シンボリックリンクだから当たり前だけど・・・)
これまでのバラバラのリポジトリでのモジュール開発ではあっちこっちpullしてビルドして、エラー出たら依存してるバージョン上げてまたビルドして・・・大変でした。
**いや、素晴らしいですね！**

# 2. いよいよ TypeScript

それではいよいよ Typescript で Workspace に挑戦してみます。
[上記のサンプルリポジトリで紹介されている手順](https://github.com/Quramy/npm-ts-workspaces-example#workspaces) を参考にやってまいりましょう。

## 2-1. プロジェクトの階層構造を構築

まずは以下の手順でプロジェクトのディレクトリ構成を js の時と同じ手順で構築します。

```bash
$ mkdir sample_ts_ws && cd $_
$ npm init -y
$ mkdir app
$ mkdir -p lib/component-a
$ mkdir -p lib/component-b
$ cd app && npm init -y
$ cd ../lib/component-a && npm init -y
$ cd ../component-b && npm init -y
$ cd ../..
``` 

はい、以下のようになりますね。

結果、以下のようなフォルダ構成のプロジェクトが出来上がります。

```
sample_ts_ws
  ├── app
  │　  └── package.json
  ├── lib
  │　  ├── component-a
  │　  │   └──  package.json
  │　  └── component-b
  │　      └──  package.json
  └──  package.json
```

## 2-2. ワークスペースの宣言

トップの package.json に `workspaces` を追加します。

```json:sample_ts_ws/package.json
{
...
  "workspaces": [
    "libs/*",
    "app"
  ]
}
```

続いて`npm i`を叩きます。ここまでの結果は同じです。

```bash
sample_ts_ws $ npm ls
sample_ts_ws@1.0.0 /xxxx/xxxxx/sample_ts_ws
├── app@1.0.0 extraneous -> /xxxx/xxxxx/sample_ts_ws/app
├── component-a@1.0.0 extraneous -> /xxxx/xxxxx/sample_ts_ws/lib/component-a
└── component-b@1.0.0 extraneous -> /xxxx/xxxxx/sample_ts_ws/lib/component-b

npm ERR! code ELSPROBLEMS
npm ERR! extraneous: app@1.0.0 /xxxx/xxxxx/sample_ts_ws/node_modules/app
npm ERR! extraneous: component-a@1.0.0 /xxxx/xxxxx/sample_ts_ws/node_modules/component-a
npm ERR! extraneous: component-b@1.0.0 /xxxx/xxxxx/sample_ts_ws/node_modules/component-b
...
```

あの。。。なんかエラー出てるんすけど([NPM problem: npm ERR! extraneous](http://lifeonubuntu.com/npm-problem-npm-err-extraneous/))。。。

依存関係がうまく認識されていないっぽいので明示的に依存を追加しました。さっきは大丈夫だったんだけどなぁ。。。なんかあるのでしょう。。。

```json:sample_ts_ws/package.json
{
   ....
  ,
  "dependencies": {
    "app": "^1.0.0",
    "component-a": "^1.0.0",
    "component-b": "^1.0.0"
  }
}
```

まぁ、これもOKですね。

## 2-3. TypeScript環境の構築

で、TypeScriptの環境を突っ込んでいきます。
TypeScriptパッケージのインストールと`tsconfig.json`の作成をします。

```
sample_ts_ws $ npm i -D typescript
...
sample_ts_ws $ npx tsc --init
message TS6071: Successfully created a tsconfig.json file.
```

### 2-3-1. トップのプロジェクトの設定調整

トップのプロジェクトの設定変更です。
以下のように、"compilerOptions"の`"composite": true` をコメントアウトを解除して有効にします。

```json:sample_ts_ws/tsconfig.json
{
  "compilerOptions" : {
....
    "composite": true,                           /* Enable project compilation */
....
  }
}
```

これがTypescript設定の起点となります。
また"tsconfig.json"とは別にビルド用の設定 "tsconfig.build.json"を作成します。こちらには全ての子コンポーネントを参照に追加しておきます。

```json:sample_ts_ws/tsconfig.build.json
{
    "files": [],
    "references": [{ "path": "app" }, { "path": "lib/component-a" }, { "path": "lib/component-b" }]
}
```
そしてこのファイルを参照するビルドスクリプトを package.json に追加しましょう。

```json:json:sample_ts_ws/package.json
{
...
  "scripts": {
    "compile": "tsc -b tsconfig.build.json",
  },
...
}
```

トップのプロジェクトはこれでOKです。

### 2-3-2. ワークスペース側のプロジェクトの設定調整

続いてコンポーネント側に tsconfig.json を追加していきます。

まず、`lib/component-a` と `component-b` は依存関係がないので、以下のようなシンプルな `tsconfig.json` でOKです。

```json:lib/component-a/tsconfig.jsonとlib/component-b/tsconfig.json
{
    "extends": "../../tsconfig.json",
    "compilerOptions": {
      "rootDir": "src",
      "outDir": "dist"
    }
}
```
`extends`でトップのtsconfig.jsonを継承しています。
続いて `app` には上記のコンポーネントを参照した`tsconfig.json`を追加します。

```json:app/tsconfig.json
{
    "extends": "../tsconfig.json",
    "compilerOptions": {
      "rootDir": "src",
      "outDir": "dist"
    },
    "references": [{ "path": "../lib/component-a" }, { "path": "../lib/component-b" }]
}
```

で、今回は `outDir` に `dist` を指定しました。このパスと package.json の `main` 属性は合わせておかないとビルドが通らないので以下のように合わせておきましょう。


```json:lib/component-a/package.jsonとlib/component-b/package.json
{
...
  "main": "dist/index.js",
...
}
```

## 2-4. 動作確認用プログラムの追加

続いてtsのプログラムを作成します。

まずはコンポーネント側から

```ts:lib/component-a/src/index.ts
export const ComponentA = {
    A : 'A だよ！'
};
```

```ts:lib/component-b/src/index.ts
export const ComponentB = {
    B : 'B どす〜'
};
```

もはやscrフォルダを作る方がめんどくさい位ですが。。。
で、このコンポーネントを参照する `app` 側のコードを作成します。

```ts:app/src/index.ts
import { ComponentA } from "component-a";
import { ComponentB } from "component-b";

console.log("start app");

console.log(ComponentA.A);
console.log(ComponentB.B);
```

VS Code 上では`import` のところで"component-a"と"component-b"ともにコード補完は効くのですが、型が見つからないエラーが出てしまいます。

## 2-5. 動作確認

これは現時点では放置でコンパイルと動作確認をしましょう。

まずは現時点でのファイル構成をおさらいしておきます。

```bash
sample_ts_ws $ tree -I node_modules
.
├── app
│   ├── package.json
│   ├── src
│   │   └── index.ts
│   └── tsconfig.json
├── lib
│   ├── component-a
│   │   ├── package.json
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   └── component-b
│       ├── package.json
│       ├── src
│       │   └── index.ts
│       └── tsconfig.json
├── package.json
├── package-lock.json
├── tsconfig.build.json
└── tsconfig.json

7 directories, 13 files
```
`node_modules`を除外すると上記のような構成になっているかと思います。
それでは以下のコマンドでコンパイルします。

```bash
sample_ts_ws $ npm run compile

> sample_ts_ws@1.0.0 compile
> tsc -b tsconfig.build.json
```

続いて、生成された index.js を叩いてみます。

```bash
sample_ts_ws $ node app/dist/index.js
start app
A だよ！
B どす〜
sample_ts_ws $ cd app && node dist/index.js
start app
A だよ！
B どす〜
```

ということで無事に module の export がちゃんと効いていることが確認できました！

## 番外. VS Code でのエラー解消・・・？

コマンドラインではちゃんと動いていますが、手元の環境の VS Code では相変わらず"型が見つからないエラー"が出ています。
自分の場合は `app/tsconfig.json` の**設定を適当にいじってエラーを起こし元に戻す** で index.ts 側のエラーの表示が消えました。何かしらキャッシュ的な問題があるのでしょう。node v15から対応したばかりだし、ということで大目に見ておきましょうか。。。

それでは、今回は以上といたします。