NPM にもやってきましたワークスペース
YarnやLernaでお馴染みのワークスペース管理ですが、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
を追加します。
{
...
"workspaces": [
"libs/*",
"app"
],
...
}
上記のような指定で、app
と libs
以下の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
のような感じで app
と component-a
、component-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
このような感じで、app
と libs
以下の component-a
、component-b
が個別のモジュールとして依存関係に追加されているのが確認できました。
1-3. 動作確認用プログラムの追加
プログラム上、ちゃんと認識されているのか、確認してみたいと思います。
シンプルなスクリプトを以下のように作成します。
component-a には以下の index.jsを作成してみます。
module.exports = 'A だよ!';
続いて component-b には以下の index.jsを作成してみます。
module.exports = 'B どす〜';
そしてこれらをappから呼び出してみましょう。
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-a
とcomponent-b
が読み込めています。
app/package.json の Dependencies には component-a
やcomponent-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
を追加します。
{
...
"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)。。。
依存関係がうまく認識されていないっぽいので明示的に依存を追加しました。さっきは大丈夫だったんだけどなぁ。。。なんかあるのでしょう。。。
{
....
,
"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
をコメントアウトを解除して有効にします。
{
"compilerOptions" : {
....
"composite": true, /* Enable project compilation */
....
}
}
これがTypescript設定の起点となります。
また"tsconfig.json"とは別にビルド用の設定 "tsconfig.build.json"を作成します。こちらには全ての子コンポーネントを参照に追加しておきます。
{
"files": [],
"references": [{ "path": "app" }, { "path": "lib/component-a" }, { "path": "lib/component-b" }]
}
そしてこのファイルを参照するビルドスクリプトを package.json に追加しましょう。
{
...
"scripts": {
"compile": "tsc -b tsconfig.build.json",
},
...
}
トップのプロジェクトはこれでOKです。
2-3-2. ワークスペース側のプロジェクトの設定調整
続いてコンポーネント側に tsconfig.json を追加していきます。
まず、lib/component-a
と component-b
は依存関係がないので、以下のようなシンプルな tsconfig.json
でOKです。
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
}
}
extends
でトップのtsconfig.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
属性は合わせておかないとビルドが通らないので以下のように合わせておきましょう。
{
...
"main": "dist/index.js",
...
}
2-4. 動作確認用プログラムの追加
続いてtsのプログラムを作成します。
まずはコンポーネント側から
export const ComponentA = {
A : 'A だよ!'
};
export const ComponentB = {
B : 'B どす〜'
};
もはやscrフォルダを作る方がめんどくさい位ですが。。。
で、このコンポーネントを参照する app
側のコードを作成します。
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から対応したばかりだし、ということで大目に見ておきましょうか。。。
それでは、今回は以上といたします。