こんにちは。
エンジニアの辻です。
今回はタイトル通り、yarn workspaceを使ってみました。
とても良い機能でしたので、備忘録を兼ねてブログにまとめてみました。
yarn workspaceとは?
yarn workspaceとは、複数のプロジェクトを1つのリポジトリでまとめて管理できる機能です。
いわゆるモノレポと呼ばれる環境を作れます。
例えば、あるプロジェクトAがあるとします。
プロジェクトAは、もともとクライアントとサーバーの2つのリポジトリがあるだけでした。
…が、プロジェクトの拡張に伴い、新機能用に新しいリポジトリを立てたり、メジャーバージョンアップのために新しいリポジトリを立てたりなど…、
時間が経過するにつれて、管理するリポジトリが増えてしまう…といった事、よくあると思います。
こういった時にyarn workspaceは、非常に便利です。
1つのリポジトリの中で、各プロジェクトを一括管理できます。
実現したかったこと
yarn workspace を使って、実現したかったのがTypeScriptの関数や型を各ワークスペースで共有する事です。
構成は以下のようになります。
この構成にすることで、/packages/common/ の中に格納してあるコードをclient01 / client02 / serverで共有できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
root ├── package.json └── packages ├── common … client01 / client02 / server で共有するTypeScriptコードを格納します。 │ ├── functions … 共有する関数を格納します │ └── types … 共有する型を格納します │ ├── client01 … クライアントサイドワークスペース1。今回はNext.jsを入れます。 │ ├── package.json │ └── tsconfig.json │ ├── client02 … クライアントサイドワークスペース2。こちらもNext.jsです。 │ ├── package.json │ └── tsconfig.json │ └── server … サーバーサイドワークスペース。Expressを入れます。 ├── package.json └── tsconfig.json |
今回は、TypeScriptのコードだけを各ワークスペースで共有しますが、やろうと思えば、CSS(SCSS)やコンポーネント、テストコードの共有なども可能です。(環境構築に時間がかかるかもですが…。)
より高度な事を実現したい場合は、NxやLernaなどのモノレポ作成ツールがありますので、そちらを活用してみると良いでしょう。
yarn workspace によるモノレポ作成の手順
では、実際にモノレポ環境を作成していきます。
以下に大まかな手順をまとめました。これに沿っていくとモノレポ環境を作成できます。
- ルートプロジェクトの用意
- packagesディレクトリを作成
- packagesディレクトリ内に各ワークスペースを作成
- packagesディレクトリ内にcommonディレクトリを作成
- ルートディレクトリのpackage.jsonにprivate/workspaceを追加
- クライアントサイドワークスペース(client01/client02)を整備
- サーバーサイドワークスペース(server)を整備
- ルートディレクトリのpackage.jsonにscriptを追加
- 最終調整をして完成
ルートプロジェクトの用意
まずプロジェクトのルートディレクトリを作成しましょう。
そして、作成したディレクトリに移動して、プロジェクトを初期化しましょう。
以下のコマンドを順に実行していけばOKです。
1 |
mkdir root |
1 |
cd root |
1 |
yarn init -y |
packagesディレクトリを作成
次に、rootディレクトリ直下にpackagesディレクトリを作成します。
packages作成後は、packagesに移動しましょう。
1 |
mkdir packages && cd packages |
packagesディレクトリ内に各ワークスペースを作成
packagesディレクトリ内に、各ワークスペースを作成していきましょう。
まずクライアントサイドワークスペース(その1)を作成します。
今回はNext.jsを使用します。以下のコマンドを実行します。
1 |
npx create-next-app client01 --typescript |
上記のコマンドが完了しましたら、クライアントサイドワークスペース(その2)を作成しましょう。
以下のコマンドを実行します。
1 |
npx create-next-app client02 --typescript |
さて、上記のコマンドが完了しましたら、次はサーバーサイドワークスペースを用意します。
まずserverディレクトリを作成して、その中に入ります。
1 |
mkdir server && cd server |
次にサーバーサイドワークスペースを初期化します。
1 |
yarn init -y |
packagesディレクトリ内にcommonディレクトリを作成
クライアントサイド/サーバーサイドで共通のコードを格納するcommonディレクトリを作成します。
1 |
mkdir common |
commonディレクトリの中に、typesとfunctionsディレクトリを作成します。
typesには共通の型ファイルを格納し、functionsには共通の関数を格納していきます。
1 |
mkdir common/types && mkdir common/functions |
では、共通化するファイルを用意していきましょう。
root/packages/common/typesにuser.tsを用意します。内容は以下の通りです。
▼root/packages/common/types/user.ts
1 2 3 4 5 |
export type UserType = { id: number; name: string; email: string; }; |
root/packages/common/functionsには、getHello.tsを用意します。内容は以下の通りです。
なんてことはない。ただ「Hello!!」を返すだけの関数ですね。
▼root/packages/common/functions/getHello.ts
1 |
export const getHello = () => 'Hello!!'; |
ルートディレクトリのpackage.jsonにprivate/workspaceを追加
ここまで来ましたら、一度rootディレクトリに戻りましょう。
そして、root直下のpackage.jsonに項目を追記していきます。
▼root/package.json
1 2 3 4 5 6 7 8 9 10 11 |
{ "name": "root", "version": "1.0.0", "license": "MIT", "private": true, // ←追加 "workspaces": { // ←追加 "packages": [ "packages/*" ] } } |
追加した項目は、privateとworkspacesです。
yarn workspace を使う場合は、ルートディレクトリ直下のpackage.jsonに必須で記述しなければなりません。
privateは必ずtrueにしてください。
workspacesにはワークスペースとするプロジェクトを指定します。
今回の場合は、packages/client01、packages/client02、packages/serverを直接指定しても良いのですが…、
workspacesはワイルドカードが使用できるため、”packages”: [ “packages/*” ] としています。
これで、packages配下のディレクトリがワークスペースとして扱われます。
ちなみに、workspacesには、packagesの他にnohoistも指定できます。
nohoistに指定したnodeモジュールは、各ワークスペースごとに個別で管理されます。
nohoistがない、もしくはnodeモジュールを指定していない場合は、各ワークスペースでnodeモジュールが共有化されます。
クライアントサイドワークスペース(client01/client02)を整備
今度は、クライアントサイドワークスペース2つ分を調整していきます。
まず、next.config.jsの内容を以下のように変更します。
やっている事は、next.js内で使用するパスのエイリアスの指定です。
「^@」でroot/packages/common/配下のファイルを参照するように設定します。
▼root/packages/client01/next.config.js・root/packages/client02/next.config.js
1 2 3 4 5 6 7 8 9 10 |
const path = require('path') /** @type {import('next').NextConfig} */ module.exports = { webpack: (config) => { config.resolve.alias['^@'] = path.resolve(__dirname, '../common') return config }, reactStrictMode: true, } |
tsconfig.jsonにbaseUrl、pathsを追記します。
追記した内容は、next.config.jsと同様で、パスエイリアスの設定です。
▼root/packages/client01/tsconfig.json・root/packages/client02/tsconfig.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
{ "compilerOptions": { "target": "es5", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "baseUrl": "./", // ← 追記 "paths": { // ← 追記 "^@/*": ["../common/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] } |
次に、root/packages/client01/pages/index.tsxとroot/packages/client02/pages/index.tsxを編集していきます。
Next.jsアプリを作成した時に、用意されているページですね。
▼root/packages/client01/pages/index.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
import { useEffect, useState } from 'react'; import type { NextPage } from 'next'; import styles from '../styles/Home.module.css'; // functions import { getHello } from '^@/functions/getHello'; // ← common/functionsからgetHelloをimport // types import { UserType } from '^@/types/user'; // ← common/typesからUserTypeをimport const Home: NextPage = () => { const [users, setUsers] = useState<UserType[]>([]); useEffect(() => { // ← サーバーサイドと連携する fetch('http://localhost:4000/') .then(async (response) => { const users: UserType[] = await response.json(); setUsers(users); }) .catch((error) => { console.log('error: ', error); }); }, []); return ( <div className={styles.container}> <h1>{getHello()} This is client02.</h1> <ul> {users.length > 0 && users.map((user) => { return ( <li key={user.id}> id: {user.id} | name: {user.name} | email: {user.email} </li> ); })} </ul> </div> ); }; export default Home; |
先程、クライアントサイドのnext.config.jsとtsconfig.jsonを調整したおかげで、common配下のファイルをimportできるようになっています。
以下のように、import文で呼び出して、ページ内で使っていますね。
1 2 |
import { getHello } from '^@/functions/getHello'; import { UserType } from '^@/types/user'; |
また、useEffectを使ったコードも実装していますね。
この部分は後ほど解説します。
1 2 3 4 5 6 7 8 9 10 |
useEffect(() => { // ← サーバーサイドと連携する fetch('http://localhost:4000/') .then(async (response) => { const users: UserType[] = await response.json(); setUsers(users); }) .catch((error) => { console.log('error: ', error); }); }, []); |
root/packages/client02/pages/index.tsxも同様に変更します。
内容はほぼ一緒ですが、client01との違いがわかるように、h1の中身を「{getHello()} This is client02.」としておきます。
▼root/packages/client02/pages/index.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
import { useEffect, useState } from 'react'; import type { NextPage } from 'next'; import styles from '../styles/Home.module.css'; // functions import { getHello } from '^@/functions/getHello'; // types import { UserType } from '^@/types/user'; const Home: NextPage = () => { const [users, setUsers] = useState<UserType[]>([]); useEffect(() => { fetch('http://localhost:4000/') .then(async (response) => { const users: UserType[] = await response.json(); setUsers(users); }) .catch((error) => { console.log('error: ', error); }); }, []); return ( <div className={styles.container}> <h1>{getHello()} This is client02.</h1> <ul> {users.length > 0 && users.map((user) => { return ( <li key={user.id}> id: {user.id} | name: {user.name} | email: {user.email} </li> ); })} </ul> </div> ); }; export default Home; |
これで、クライアントサイドワークスペースの調整は完了です。
サーバーサイドワークスペース(server)を整備
次にサーバーサイドワークスペースを調整していきます。
まずカレントディレクトリをrootにします。その後に、以下のコマンドを実行しましょう。
このコマンドを実行すると、root/packages/serverにnodeモジュールがインストールされます。
1 |
yarn workspace server add @types/express @types/node express ts-node typescript |
yarn workspace を使う場合、コマンドの形式は以下のようになります。
サーバーサイドワークスペース(serverディレクトリ配下)に、Expressをはじめとしたnodeモジュールをインストールしたかったので、上記のようなコマンドになったわけです。
1 |
yarn workspace <workspace名> <コマンド名> <引数…> |
ちなみに、クライアントサイドワークスペースへ何かのnodeモジュールを追加したい・削除したい場合は、以下のようになります。
▼client01へnodeモジュールを追加
1 |
yarn workspace client01 add hoge-module |
▼client01のnodeモジュールを削除
1 |
yarn workspace client01 remove fuga-module |
さて、nodeモジュールインストールが完了しましたら、サーバーサイドワークスペースのファイルを整備していきましょう。
まずtsconfig.jsonを作成します。
srcにTypeScriptファイルを配置するので、includeは忘れないように記述しておきましょう。
▼root/packages/server/tsconfig.json
1 2 3 4 5 6 7 8 9 10 11 12 |
{ "compilerOptions": { "target": "ES2019", "module": "commonjs", "sourceMap": true, "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"] } |
次に、server直下にsrcディレクトリを作成し、その中にmain.tsを格納しまししょう。
内容は以下の通りです。
▼root/packages/server/src/main.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// module import express from 'express'; // const const port = 4000; // types import { UserType } from '../../common/types/user'; // functions import { getHello } from '../../common/functions/getHello'; // user const users: UserType[] = [ { id: 1, name: 'hoge', email: 'hoge@test.co.jp' }, { id: 2, name: 'fuga', email: 'fuga@test.co.jp' }, { id: 3, name: 'piyo', email: 'piyo@test.co.jp' }, ]; // express const app: express.Express = express(); app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { res.header('Access-Control-Allow-Origin', '*'); next(); }); // ユーザーを返す app.get('/', (req: express.Request, res: express.Response) => { res.send(users); }); // start app.listen(port, () => { console.log(`${getHello()} port: ${port}.`); }); |
root/packages/server/src/main.ts でやっている事はシンプルです。
expressを使って、http://localhost:4000/へGETで叩くと、ユーザー一覧を返すだけのコードです。
先程、クライアントサイドワークスペースのindex.tsxで後ほど解説すると言っていた点がここです。
サーバーサイドでは、http://localhost:4000/(GET)でユーザー一覧を返すようになっています。
クライアントサイドでは、useEffectを使って初期表示時に、http://localhost:4000/を叩いてユーザー一覧を取得して、ページに表示するようにしています。
クライアントサイドワークスペース同様、packages/commonから、UserType・getHelloをimportして、それぞれ使っています。
これでクライアントサイドとサーバーサイドでコードの共有化ができましたね。
さて、次は、server直下のpackage.jsonを編集します。
以下のように、scriptsを入れましょう。
▼root/packages/server/package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
{ "name": "server", "version": "1.0.0", "license": "MIT", "dependencies": { "@types/express": "^4.17.13", "@types/node": "^16.11.7", "express": "^4.17.1", "ts-node": "^10.4.0", "typescript": "^4.4.4" }, "scripts": { ← // 追記 "dev": "npx ts-node ./src/main.ts" } } |
ルートプロジェクトのpackage.jsonにscriptを追加
あともうちょっとです。
ルートディレクトリ直下のpackage.jsonにscriptsを追記します。
▼root/package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
{ "name": "root", "version": "1.0.0", "license": "MIT", "private": true, "scripts": { // ←追記 "cl01:dev": "yarn workspace client01 dev", "cl02:dev": "yarn workspace client02 dev", "se:dev": "yarn workspace server dev" }, "workspaces": { "packages": [ "packages/*" ] } } |
先程、チラッと解説しましたが、yarn workspace を使った時のコマンドは以下のようになります。
1 |
yarn workspace <workspace名> <コマンド名> <引数…> |
ですので…、
yarn workspace client01 devを実行すると、packages/client01のNext.jsのdevコマンドを実行されます。
同じように、yarn workspace client02 devは、packages/client02のNext.jsのdevコマンドが。
yarn workspace server devは、packages/serverのdevコマンドが実行されます。
これらのコマンドをルートディレクトリのpackage.jsonに登録しておくことで、ルートディレクトリから各ワークスペースのコマンドを実行できるようにしています。
クライアントサイド(その1)のdevを実行したいなら、yarn cl01:dev。
サーバーサイドのdevを実行したいなら、yarn se:devを使えばOKです。
最終調整をして完成
さて、ここまでくれば完成!…と言いたいのですが、
各ワークスペースのpackage.jsonに足りない項目があると、yarn workspaceコマンドがうまく実行されない場合があります。
私が環境構築した際は、各ワークスペースのpackage.jsonに「version」がないために、yarn workspaceコマンドがうまく動作しませんでした。
また、package.jsonの「name」も重要な項目です。
ここまで触れませんでしたが、実はpackage.jsonのnameは、yarn workspaceコマンドのworkspace名と一致させる必要があります。
でないと、yarn workspaceコマンドがうまく動作しません。
他にもいろいろありそうですが、とりあえず…、
各ワークスペースのpackage.jsonにWARNINGが出ないようにする。
各ワークスペースのpackage.jsonのnameは、yarn workspaceコマンドのworkspace名と一致させる。
…の2つをクリアしていれば問題はないかと思います。
一度、各ワークスペースのpackage.jsonを見直しましょう。
最終調整が終わりましたら、これでモノレポ環境作成作業は完了です!
お疲れさまでした。
ルートディレクトリをカレントとして、yarn cl01:devとyarn se:devのコマンド2つ分を実行してみましょう。
その上で、http://localhost:3000/を開くと、サーバーサイドからレスポンスが返され、ユーザー一覧が画面に表示されると思います。
また、yarn cl02:devでも同じように表示できるようになっていると思います。
このようにyarn workspaceを使うと、モノレポ環境を作成することができます。
各ワークスペースで簡単にコードを共有できますので、大変便利な機能です。
今回はここまで。
では、また!