monorepo環境で、yarn workspace を使って、create-react-app --typescript した client ワークスペースと、prisma2 で作成した server ワークスペースを共存させる手順のメモです。Lint, Prettier, Huskyの設定も行います。
前提条件
完成品
手元で同様の手順を踏んだリポジトリを、GitHub上に公開していますので、併せてご参照ください。
動作環境
- Mac
- Node.js v10.16.0
- npm v6.9.0
- create-react-app (react-script v3.1.1)
- prisma2 v2.0.0-preview-5
- TypeScript v3.5.3
- Lint, Prettier, Husky
<概説>ワークスペース(Yarn Workspaces)とは?
デフォルトで利用できるパッケージのアーキテクチャを設定する新しい方法です。ワークスペースにより複数のパッケージを設定する際に、 yarn install を一度実行するだけで、それらの全てが単一のパスにインストールされるようになります。
1つのプロジェクトを立ち上げるとき、クライアント・サーバ・共通ロジック・Lambda・デザインシステム・LPなどの様々なサブプロジェクトが必要になることは多いと思います。これらを1つのリポジトリで扱えるようにする考え方が monorepo であり、それを実現する手段がワークスペースとなります。
monorepo環境の管理には、現在においては Lerna などが方法として存在しますが、ワークスペースはより低レベルでプリミティブな、内部依存関係の解決に特化した仕組みを提供してくれるものです。
セットアップ: yarn workspace
リポジトリ作成
ハンズオン形式で進めます。まずは簡単にリポジトリの作成から;
$ mkdir monorepo-react-prisma2
$ cd monorepo-react-prisma2
$ git init
$ yarn init -y
# dependencies
node_modules
# misc
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package.json で workspace を設定
yarn workspace で monorepo環境を構築する場合は、package.jsonに private: true
と workspaces: {...}
の2つを記述することになっています。
-
packages:
ワークスペースの対象となるディレクトリを指定します。ワイルドカード指定が可能です。 -
nohoist:
指定したnpmモジュールを、子ワークスペースごとで個別に管理させることができるようになります。指定していなかったnpmモジュールは、ワークスペース間で共用利用になり、ディスク容量が削減できるなどのメリットを得ることができます。
{
"name": "monorepo-react-prisma2",
"version": "1.0.0",
"main": "index.js",
"author": "suzukalight <mail@mkubara.com>",
"license": "MIT",
"private": true,
"workspaces": {
"packages": [
"src/*"
],
"nohoist": [
"**/react-scripts",
"**/react-scripts/**",
"**/@generated",
"**/@generated/**"
]
}
}
client: create-react-app --typescript
ワークスペース作成
create-react-app で TypeScript の環境を作成します。(ejectは使用しません);
$ npx create-react-app src/client --typescript
yarn workspace client add
個別のワークスペースでのみ使用するnpmモジュールをインストールする場合は、yarn workspace [ws-name] add
コマンドを使用します。
ここでは、client側でしか使わないようなパッケージのインストールを行うために、yarn workspace client add
コマンドを使用してみます;
$ yarn workspace client add node-sass
成功すると、client側のpackage.jsonにのみ、設定が追加されます;
{
"dependencies": {
"node-sass": "^4.12.0",
},
}
yarn workspace client start
個別のワークスペースに記述したnpmスクリプトは、yarn workspace [ws-name] [script-name]
コマンドで起動することができます。
この仕組みを利用して、ルートのpackage.jsonに、clientのdev-server起動コマンドを追加します;
{
"scripts": {
"cl:start": "yarn workspace client start",
},
}
これでクライアントの開発環境が起動できるようになりました;
$ yarn cl:start
server: Prisma2
今回は手軽にGraphQLサーバを立てられる Prisma2 と graphql-yoga を利用して、簡単な手順でサーバを構築しています。
ワークスペース作成
サーバ環境のディレクトリを作成します;
$ mkdir src/server
$ cd src/server
Prisma2 の CLI が未インストールの場合は、それもインストールしてください;
$ npm i -g prisma2@2.0.0preview-5
prisma2 は、2019年末のリリースに向けて、絶賛開発中です。そのため Breaking Changes が常に発生しており、執筆時点の preview-9.1 では PhotonJS のサンプルが正常に起動しませんでした。このため、バージョンを固定してインストールしています。
PhotonJS のサンプル から、5つのファイルをコピーし、初期環境としました;
- src/server/
- prisma/
- schema.prisma
- seed.ts
- src/
- index.ts
- types.ts
- tsconfig.json
パッケージのインストール
package.json に dependencies などを追加していきます。このように yarn workspace [ws-name] add
を通さず、直接package.jsonを書き換えてもOKです。
{
"name": "server",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "ts-node-dev --no-notify --respawn --transpileOnly ./src",
"seed": "ts-node prisma/seed.ts",
"postinstall": "prisma2 generate"
},
"dependencies": {
"@prisma/nexus": "^0.0.1",
"graphql-yoga": "1.17.4",
"nexus": "0.11.7"
},
"devDependencies": {
"@types/node": "10.14.9",
"ts-node": "^8.3.0",
"ts-node-dev": "1.0.0-pre.40",
"typescript": "3.5.2"
}
}
変更したpackaje.jsonに基づいて、ワークスペースのnpmパッケージをインストールします;
$ yarn
photon + lift によるサーバの自動生成
-
prisma2 lift
: マイグレーションファイルを生成し、そのファイルを利用してマイグレーションを実行します -
prisma2 generate
: このプロジェクト用の PhotonJS を生成します -
yarn seed
: seed.ts に記述した初期データを流し込みます
$ prisma2 lift save --name 'init'
$ prisma2 lift up
$ prisma2 generate
$ yarn seed
サーバ起動
ルートのpackage.jsonに、サーバ起動コマンドを追加します;
{
"scripts": {
"sr:start": "yarn workspace server start",
},
}
これでサーバの開発環境が起動できるようになりました;
$ cd ../../
$ yarn sr:start
Lint + Prettier
コードの健全性を高めるために、すべてのワークスペースで、LintやPrettierを実行します。すべてのワークスペースで行うため、これらを共用モジュールとしてインストールします。
コーディングルールについては、各ワークスペースごとに個別の設定を行いたい場合は、各々のワークスペースに配置します。そうでないものについては、ルートディレクトリに配置します。
yarn install
- eslint, prettier のインストール
- 各種 eslint-plugin のインストール
- TypeScript のために
@typescript-eslint/eslint-plugin
@typescript-eslint/parser
を追加
$ yarn add -D -W prettier eslint eslint-config-prettier eslint-plugin-prettier
$ yarn add -D -W eslint-plugin-import eslint-plugin-flowtype eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks
$ yarn add -D -W @typescript-eslint/eslint-plugin @typescript-eslint/parser
package.json
Lint+Prettier を行うスクリプトを、ルートのpackage.jsonに記述しておきます;
{
"scripts": {
"lint": "yarn cl:lint && yarn sr:lint",
"cl:lint": "eslint --fix --ext .jsx,.js,.tsx,.ts ./src/client/src",
"sr:lint": "eslint --fix --ext .jsx,.js,.tsx,.ts ./src/server/src",
},
}
設定ファイルの配置
.prettierrc
, .eslintrc.json
, tsconfig.json
を追加していきます。
.prettierrc
.prettierc は、ルートに同じ内容を配置しました;
{
"printWidth": 100,
"tabWidth": 2,
"trailingComma": "all",
"singleQuote": true,
"semi": true
}
tsconfig.json
tsconfig.json は client と server で異なります。いずれもすでに作成済みのものをそのまま利用します。
ルートディレクトリの tsconfig.json には、共通設定を置くのですが、ここは無指定とします(配置しないと lint 時にエラーが出たため、ダミーとして配置しています);
{}
client 側は、create-react-app が提供しているファイルをそのまま利用します。React を意識した設定です;
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react"
},
"include": [
"src"
]
}
server 側は、Prisma2 が提供しているファイルをそのまま利用します。ts-node を意識した設定です;
{
"compilerOptions": {
"sourceMap": true,
"outDir": "dist",
"lib": ["esnext", "dom"],
// "strict": true, // `strict` is commented because of this issue: https://github.com/prisma/prisma/issues/3774; all its options except `strictNullChecks` & `strictPropertyInitialization` are explicitly set below.
"noImplicitAny": true,
"noImplicitThis": true,
"alwaysStrict": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
"skipLibCheck": true, // `skipLibCheck` is enabled until this is issue is fixed: https://github.com/prisma/nexus-prisma/issues/82,
"esModuleInterop": true
}
}
.eslintrc.json
基本設定は以下の通りとしました;
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"prettier/@typescript-eslint"
],
"plugins": ["@typescript-eslint", "prettier"],
"env": { "node": true, "es6": true },
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module",
"project": "./tsconfig.json"
},
"rules": {
"prettier/prettier": "error",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-var-requires": "off"
}
}
これとは別に、client側のlint設定に、React関係のプラグイン設定を追加し、配置しました;
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"prettier/@typescript-eslint"
],
"plugins": ["@typescript-eslint", "prettier", "react"],
"env": { "node": true, "es6": true },
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module",
"project": "./tsconfig.json"
},
"rules": {
"prettier/prettier": "error",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-var-requires": "off",
"react/jsx-uses-vars": "warn",
"react/jsx-uses-react": "warn"
}
}
Lint + Prettier 実行
これでクライアント・サーバ両方のLint+Prettierが実行できるようになりました。早速実行してみます;
$ yarn lint
> $ eslint --fix --ext .jsx,.js,.tsx,.ts ./src/client/src
/Users/suzukalight/work/monorepo-react-prisma2/src/client/src/serviceWorker.ts
45:9 error 'checkValidServiceWorker' was used before it was defined @typescript-eslint/no-use-before-define
57:9 error 'registerValidSW' was used before it was defined @typescript-eslint/no-use-before-define
もし「pluginが読み取れない」系のエラーが発生した場合は、
@typescript-eslint/eslint-plugin @typescript-eslint/parser
のバージョンを 1.3 にダウングレードしてみると良いかもしれません。$ yarn add -D -W @typescript-eslint/eslint-plugin@^1.3 @typescript-eslint/parser@^1.3
エラーが出ている部分を修正します。create-react-appの自動生成部分なので、それを信じて握りつぶします…。
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config); // eslint-disable-line @typescript-eslint/no-use-before-define
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA',
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config); // eslint-disable-line @typescript-eslint/no-use-before-define
}
});
再度実行すると、正常終了しました;
$ yarn lint
> $ eslint --fix --ext .jsx,.js,.tsx,.ts ./src/client/src
> $ eslint --fix --ext .jsx,.js,.tsx,.ts ./src/server/src
> ✨ Done in 5.40s.
ステージされたファイルの diff を見てみると、Prettier によっていくつかのファイルが自動整形されているのがわかります。これで Lint+Prettier が成功していることが確認できたと思います。
Husky + lint-staged
クライアント・サーバどちらのファイルがコミットされても、huskyによって自動的に Lint+Prettier が行われ、不健全なファイルがコミットされないように設定します;
セットアップ
$ yarn add -D -W husky lint-staged
package.json に husky と lint-staged の設定を追加します;
{
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"./src/{client,server}/src/**/*.{js,jsx,ts,tsx}": [
"eslint --fix",
"git add"
]
}
}
Lint によるコミット阻止例
lintの通らないファイルがコミットに失敗するかをテストします。App.tsxの7行目に <>
だけを追加してみます;
import React from 'react';
import logo from './logo.svg';
import './App.css';
const App: React.FC = () => {
return (
<>
<div className="App">
<header className="App-header">
コミットしてみると、エラーとなってコミットに失敗しています;
$ git commit -am "wrong"
husky > pre-commit (node v10.16.0)
↓ Stashing changes... [skipped]
→ No partially staged files found...
❯ Running tasks...
❯ Running tasks for ./src/{client,server}/src/**/*.{js,jsx,ts,tsx}
✖ eslint --fix
git add
✖ eslint --fix found some errors. Please fix them and try committing again.
/Users/suzukalight/work/monorepo-react-prisma2/src/client/src/App.tsx
6:10 error Parsing error: JSX fragment has no corresponding closing tag
✖ 1 problem (1 error, 0 warnings)
Prettier によるコミット前の自動整形例
Prettier によって自動整形される例もテストします。さきほどのApp.tsxの <>
を空行に置き換えます;
import React from 'react';
import logo from './logo.svg';
import './App.css';
const App: React.FC = () => {
return (
<div className="App">
<header className="App-header">
コミットしてみると、空行が取り除かれた状態でファイルが保存され、コミットされていることが確認できたと思います。これで成功です!
mkubarambp2018:monorepo-react-prisma2 mkubara$ git commit -am "auto format"
husky > pre-commit (node v10.16.0)
↓ Stashing changes... [skipped]
→ No partially staged files found...
✔ Running tasks...
[master 6df70da] auto format
完成品
手元で同様の手順を踏んだリポジトリを、GitHub上に公開していますので、併せてご参照ください。