LoginSignup
24
11

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-04-11

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

YarnLernaでお馴染みのワークスペース管理ですが、Node v15 の nvm 7 からこれに対応した模様です!

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

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

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

1. まず JS から

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

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

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

$ 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 を追加します。

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

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

$ 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

のような感じで appcomponent-acomponent-bが認識されていることがわかります。

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

$ 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

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

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

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

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

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

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

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

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

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

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 を起動してみます。

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

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

2. いよいよ TypeScript

それではいよいよ Typescript で Workspace に挑戦してみます。
上記のサンプルリポジトリで紹介されている手順 を参考にやってまいりましょう。

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

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

$ 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 を追加します。

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

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

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)。。。

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

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 をコメントアウトを解除して有効にします。

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

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

sample_ts_ws/tsconfig.build.json
{
    "files": [],
    "references": [{ "path": "app" }, { "path": "lib/component-a" }, { "path": "lib/component-b" }]
}

そしてこのファイルを参照するビルドスクリプトを package.json に追加しましょう。

json
{
...
  "scripts": {
    "compile": "tsc -b tsconfig.build.json",
  },
...
}

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

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

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

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

lib/component-a/tsconfig.jsonとlib/component-b/tsconfig.json
{
    "extends": "../../tsconfig.json",
    "compilerOptions": {
      "rootDir": "src",
      "outDir": "dist"
    }
}

extendsでトップのtsconfig.jsonを継承しています。
続いて app には上記のコンポーネントを参照したtsconfig.jsonを追加します。

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

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

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

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

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

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

lib/component-a/src/index.ts
export const ComponentA = {
    A : 'A だよ!'
};
lib/component-b/src/index.ts
export const ComponentB = {
    B : 'B どす〜'
};

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

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. 動作確認

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

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

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を除外すると上記のような構成になっているかと思います。
それでは以下のコマンドでコンパイルします。

sample_ts_ws $ npm run compile

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

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

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から対応したばかりだし、ということで大目に見ておきましょうか。。。

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

24
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
11