複数の Nuxt.js プロジェクトでコンポーネントやロジックなどを共有するために monorepo する

ほぼチーム共有用ですが、一般的なノウハウなので Qiita に書きます。

私事ですが、Nuxt で作っている BtoBtoC サービスで「管理画面」と「その管理者を管理する管理画面」を用意する場合、ほぼ同じ画面構成・見た目という理由からリポジトリごとコピーして必要に応じて弄る、という方法がとられていました。

当たり前ですが、共通部分のコードに変更が必要になると、同じ変更を2回（実際のところ何回もコピーしたので2回じゃ済まないんですが...）やらないといけなくなっています。

あと地味にしんどいのが **dependabot のお世話**ですね。少し目を離すと彼が一番熱心なコミッターになるというのはあるあるです。

これを脱するため、monorepo にして共有できるものは共有しようという試みがこの記事です（移行のコストが地味にあって実現できるかは不明ですが...）

※余談ですが、共通化には罠もあるので取り扱いには注意ですね。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">クソコード動画「共通化の罠」 <a href="https://t.co/MM750CNXc2">pic.twitter.com/MM750CNXc2</a></p>&mdash; ミノ駆動 (@MinoDriven) <a href="https://twitter.com/MinoDriven/status/1127539251761909760?ref_src=twsrc%5Etfw">May 12, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## リポジトリ

https://github.com/kiyopikko/nuxt-monorepo-example

## 元ネタ

https://medium.com/dailyjs/5-tips-for-sharing-code-between-nuxtjs-projects-6ffb5b7f8a25 の方法を使って、より実用的なかたちで書いたのがこの記事になります。

Nuxt プロジェクトはパッケージとして publish する必要はなく、ビルドしてデプロイできればよいです。

なので、一般的な monorepo とは違い、それぞれのパッケージをひとつにまとめて publish できるようにして `lib` とかを参照するのではなく、パッケージ間でソースコードをそのまま import するやり方をとります（もはやこれを monorepo というのは謎ですが）。

## 構造

![スクリーンショット 2021-01-15 20.15.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94526/89a1346b-548e-cd7c-119e-7ec29dc91815.png)

## どういう開発体験か

プロジェクトAの開発サーバーを立ち上げてるときに、ベースプロジェクトにコンポーネントを追加すると、プロジェクトAのテンプレートHTMLですぐに使えてその変更がホットリロードされる、というとなんとなくいい感じというイメージが伝わるかなと思います。

[![Image from Gyazo](https://i.gyazo.com/c1ccb804f8d018125dd909e4370dbaa0.gif)](https://gyazo.com/c1ccb804f8d018125dd909e4370dbaa0)

## 利用ライブラリなど（create-nuxt-app で ON にした機能＋α）

- TypeScript
- ESLint
- Prettier
- Lint staged files
- StyleLint
- Commitlint
- Jest
- Semantic Pull Requests
- Dependabot
- Github Actions
- Sass（`@nuxtjs/style-resources` も）
- Composiiton API

それでは以下、手順。

## yarn をセットアップ

```
yarn init
```

適当に入力。 `package.json` が出力される。

## yarn workspaces を有効に

`private: true` にして、 `workspaces` を追加。

```diff:package.json
{
    ...,
+   "private": true,
+   "workspaces": [
+       "packages/*"
+   ]
}
```

## lerna のインストール

追加しないと差分がうるさくなるので先に `.gitignore` を追加しておきます。あとで create-nuxt-app で書き出されるもので上書きするのでとりあえずこれだけで OK。

```bash:.gitignore
node_modules
```

ルートの `pacakge.json` に `lerna` をインストール

```
yarn add -W -D lerna
```

## lerna のセットアップ

`--independent` 設定で バージョニングが個別になります。オープンソースライブラリのような「メインとそのサブパッケージ」という構成でなければ independent で良いかと。

```
yarn lerna init --independent
```

`lerna.json` が書き出されます。

yarn workspaces を使うので以下のように書き換えます。

```diff:lerna.json
{
  "packages": [
    "packages/*"
  ],
+ "npmClient": "yarn",
+ "useWorkspaces": true,
  "version": "independent"
}
```

## ベースプロジェクトの作成

ベースとなるプロジェクトを作成します。ここから Extend するかたちで他プロジェクトを作成します。

```
mkdir -p packages/base
cd packages/base
```

`create-nuxt-app` します。 `create-nuxt-app` のバージョンは `v3.4.0` です。

```
yarn create nuxt-app
```

よしなにします。 今回は以下のように。
![スクリーンショット 2021-01-14 14.17.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94526/365b56b7-9e9a-5858-2ab8-c0dd5139e859.png)

ルートで git 管理するので、 `Version control system` は `None` にします。

 `create-nuxt-app` で生成される dependencies, devDependencies はすべてのプロジェクトで共通なので、そのままルートにコピペして、ベースプロジェクトからは削除します。ルートの package.json に既に記載してある `lerna` は残しましょう。

```packages/base/package.json
{
  ...
  以下を全部ルートの package.json へ移動
  "dependencies": {
    "@nuxt/typescript-runtime": "^2.0.0",
    "core-js": "^3.6.5",
    "nuxt": "^2.14.6"
  },
  "devDependencies": {
    "@commitlint/cli": "^11.0.0",
    "@commitlint/config-conventional": "^11.0.0",
    "@nuxt/types": "^2.14.6",
    "@nuxt/typescript-build": "^2.0.3",
    "@nuxtjs/eslint-config": "^3.1.0",
    "@nuxtjs/eslint-config-typescript": "^3.0.0",
    "@nuxtjs/eslint-module": "^2.0.0",
    "@nuxtjs/stylelint-module": "^4.0.0",
    "@vue/test-utils": "^1.1.0",
    "babel-core": "7.0.0-bridge.0",
    "babel-eslint": "^10.1.0",
    "babel-jest": "^26.5.0",
    "eslint": "^7.10.0",
    "eslint-config-prettier": "^6.12.0",
    "eslint-plugin-nuxt": "^1.0.0",
    "eslint-plugin-prettier": "^3.1.4",
    "husky": "^4.3.0",
    "jest": "^26.5.0",
    "lint-staged": "^10.4.0",
    "prettier": "^2.1.2",
    "stylelint": "^13.7.2",
    "stylelint-config-prettier": "^8.0.2",
    "stylelint-config-standard": "^20.0.0",
    "ts-jest": "^26.4.1",
    "vue-jest": "^3.0.4"
  }
}
```

ちなみにこの状態（移動しなくても）でも依存パッケージはすべてルートの node_modules に入ります（ホイスティング）。どのプロジェクトもルートの node_modules を参照して動いているということを頭に入れておくと、後々良いかもしれません。

## 設定ファイルをルートに移す

`base` の設定ファイル郡を `nuxt.config.js` と `package.json` だけになるように移動します。ルートが以下のようになります。

![スクリーンショット 2021-01-14 14.21.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94526/9ed54b1a-db45-44cc-9e76-f770c3286aa0.png)

## `base` で必要な設定をする

ファイルのExtendやコマンドの修正が必要になります。

### Typescript

`tsconfig.json` を親から extend します。

```json:packages/base/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "baseUrl": "."
  }
}
```

あとで Jest のためにまた弄ることになりますが、Jest 無しならこれで大丈夫なはずです。

### ESLint

create-nuxt-app で作成したプロジェクトは `lint` スクリプトが `eslint --ext .js,.vue --ignore-path .gitignore .` になっています。

`.gitignore` は今はルートにあるので以下に変更します。

```diff:packages/base/package.json
{
 ...
-  "lint:js": "eslint --ext .js,.vue --ignore-path .gitignore .",
-  "lint:style": "stylelint **/*.{vue,css} --ignore-path .gitignore", 
+  "lint:js": "eslint --ext .js,.vue --ignore-path ../../.gitignore .",
+  "lint:style": "stylelint **/*.{vue,css} --ignore-path ../../.gitignore",
 ...
}
```

コマンドは以下で実行します。

```
yarn lerna run lint --scope base
```

### Jest

Jest に関しては、`lerna` ではなく Jest の projects 機能を使って実行します。

必要なライブラリをインストールします。`@vue/cli-service` を入れるのは、Vue プロジェクトで Jest だとカバレッジが正しくとれない問題の解決として、 `jest` コマンドの代わりに `vue-cli-service test:unit` を使うためです。

```
yarn lerna add -W -D @types/jest @vue/cli-service @vue/cli-plugin-unit-jest
```

まず、ルートの設定を以下にします。

```jest.config.js
module.exports = {
  projects: ['<rootDir>/packages/*'],
}
```

```diff:package.json
{
  ...
+ "scripts": {
+   "test:base": "vue-cli-service test:unit --projects packages/base",
+   "test": "vue-cli-service test:unit"
+ },
  ...
}

```

それぞれのプロジェクトで Extend するためのベースの設定ファイルを書きます（create-nuxt-app で書き出されたものを修正したもの）。

```jest.config.base.js
module.exports = {
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/$1',
    '^~/(.*)$': '<rootDir>/$1',
    '^vue$': 'vue/dist/vue.common.js',
  },
  moduleFileExtensions: ['ts', 'js', 'vue', 'json'],
  transform: {
    '^.+\\.ts$': 'ts-jest',
    '^.+\\.js$': 'babel-jest',
    '.*\\.(vue)$': 'vue-jest',
  },
  collectCoverage: true,
  collectCoverageFrom: ['<rootDir>/components/**/*.vue'],
  testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(js|ts)$',
  preset: 'ts-jest',
}
```

ベースプロジェクトの設定ですが、まずは `tsconfig.json` に `@types/jest` を追加します。

```diff:packages/base/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "baseUrl": ".",
+   "types": [
+     "@types/jest",
+   ]
  }
}
```

`jest.config.js` はルートの `jest.config.base.js` を Extend して名前だけつけてあげます（名前をつけないと複数プロジェクトで正しく動かなかったです）。

```packages/base/jest.config.js
const config = require('../../jest.config.base')

module.exports = { name: 'base', displayName: 'Base Tests', ...config }
```

テストも TypeScript ファイル（`*.spec.ts`）を作成する想定なので `.vue` ファイルの型を追加しましょう。

```packages/base/vue-shims.d.ts
declare module '*.vue' {
  import Vue from 'vue'
  export default Vue
}
```

使わないので `packages.json` から `test` コマンドを削除します。

```diff:packages/base/package.json
{
  ...
  "scripts": {
    ...
-    "lint": "yarn lint:js && yarn lint:style",
-    "test": "jest"
+    "lint": "yarn lint:js && yarn lint:style"
  },
  ...
}

```

コマンドは以下で実行します。

```bash
yarn test # すべてのプロジェクトをテストする場合
yarn test:base # base だけテストする場合
```

## Vetur

Vetur はデフォルトでルートの `tsconfig.json` を参照するので `vetur.config.js` でプロジェクトごとに `tsconfig.json` の参照先を指定する必要があります。

```vetur.config.js
module.exports = {
  settings: {
    'vetur.useWorkspaceDependencies': true,
    'vetur.experimental.templateInterpolationService': true,
  },
  projects: [
    {
      root: './packages/base',
      tsconfig: './tsconfig.json',
    },
  ],
}
```


## ベースプロジェクトのファイルを共有するための `module.js` を作成

[この記事](https://medium.com/dailyjs/5-tips-for-sharing-code-between-nuxtjs-projects-6ffb5b7f8a25) の通りに、以下を作成します。

```packages/base/module.js
import path from 'path'
import { createRoutes, relativeTo } from '@nuxt/utils'
import serveStatic from 'serve-static'

// ページを追加するごとに追加、動的に生成したほうがよいかと
const pages = ['pages/index.vue']

export default function NuxtModule() {
  const { routeNameSplitter, trailingSlash } = this.options.router
  this.extendRoutes((routes) => {
    routes.push(
      ...createRoutes({
        files: pages,
        srcDir: __dirname,
        pagesDir: 'pages',
        routeNameSplitter,
        trailingSlash,
      })
    )
  })

  const layoutPath = (file) =>
    relativeTo(this.options.buildDir, path.resolve(__dirname, 'layouts', file))

  this.nuxt.hook('build:templates', ({ templateVars }) => {
    templateVars.layouts.default = layoutPath('default.vue')
  })

  this.addServerMiddleware(serveStatic(path.resolve(__dirname, 'static')))

  this.nuxt.hook('components:dirs', (dirs) => {
    dirs.unshift({ path: path.resolve(__dirname, 'components'), level: 1 })
  })

  this.nuxt.hook('components:extend', (components) => {
    // If there are duplicates, give last the priority
    const noDupes = Object.values(
      components.reduce(
        (map, comp) => ({ ...map, [comp.pascalName]: comp }),
        {}
      )
    )
    components.length = 0
    components.push(...noDupes)
  })
}
```

`pages`, `layouts`, `components`, `static` が受け継ぐことができる Nuxt Module を作成できます。これを別プロジェクトの `nuxt.config.js` の `modules` に追加すると受け継げます。


## 読み込み側のプロジェクトを作成

ルートや base から設定などを引き継ぐので、create-nuxt-app は使わずに一つひとつファイルを作成します。管理画面を想定しているので `admin` という名前で作成します。

```
mkdir -p packages/admin
cd packages/admin
```

`base` から `package.json` をコピーしてきて name だけ修正します。

```packages/admin/package.json
{
  "name": "admin",
  ...
}
```

`nuxt.config.js` は `base` を流用して必要な箇所だけ変更します。 `head` は一応すべて定義し直しにしてます。

```packages/admin/nuxt.config.js
import defaultConfig from 'base/nuxt.config'

export default {
  ...defaultConfig,
  head: {
    title: 'admin',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: '' },
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
  },
}
```

`base` で必要だった作業をこちらでもします。

```packages/admin/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "baseUrl": ".",
    "types": [
      "@types/jest",
    ]
  }
}
```

```packages/admin/vue-shims.d.ts
declare module '*.vue' {
  import Vue from 'vue'
  export default Vue
}
```

```packages/admin/jest.config.js
const config = require('../../jest.config.base')

module.exports = { name: 'admin', displayName: 'Admin Tests', ...config }
```

```diff:vetur.config.js
module.exports = {
  settings: {
    'vetur.useWorkspaceDependencies': true,
    'vetur.experimental.templateInterpolationService': true,
  },
  projects: [
+   {
+     root: './packages/admin',
+     tsconfig: './tsconfig.json',
+   },
    {
      root: './packages/base',
      tsconfig: './tsconfig.json',
    },
  ],
}
```

```diff:package.json
{
  ...
  "scripts": {
    "test:base": "vue-cli-service test:unit --projects packages/base",
+   "test:admin": "vue-cli-service test:unit --projects packages/admin",
    "test": "vue-cli-service test:unit"
  },
  ...
}
```

では、`base` のファイルを受け継ぐ方法です。

## pages の共有

`base` に以下のファイルを作成して

```packages/base/pages/index.vue
<template>
  <div class="container">Hoge</div>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({})
</script>
```

`module.js` の `pages` にそのページのパスを記述すると、そのモジュールを読み込んだ先でページが表示できます。

```packages/base/module.js
...
const pages = ['pages/index.vue']
...
```

`admin` 側で `nuxt.config.js` で `base/module` を読み込みます。

```packages/admin/nuxt.config.js
{
...
  modules: ['base/module']
...
}
```

開発サーバーを起動してみましょう。

```
yarn lerna run --stream --scope admin dev
```

http://localhost:3000 を開いてみるとページが閲覧できるかと思います。

## components の共有

`base` につくったコンポーネントを共有する例です。

```packages/base/components/BaseButton.vue
<template>
  <button class="b-btn">Hoge</button>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({})
</script>
```

`base` と `admin` の `nuxt.config.js` で `components: true` になっていることを確認しましょう。以下のように記述するとボタンが表示されるかと思います。

```packages/admin/pages/index.vue
<template>
  <base-button />
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({})
</script>
```

しれっと書いてますが、 `base` と `admin` で `pages` のパスがかぶると、継承先の方が優先されます。

開発サーバーを起動してみましょう。

```
yarn lerna run --stream --scope admin dev
```

http://localhost:3000 を開いてみると、BaseButton が表示できているか思います。

## layouts の共有

`module.js` に記述している以下ですが、 `default` を `base` の `default.vue` で塗り替えるようになります。

```packages/base/module.js
...
  this.nuxt.hook('build:templates', ({ templateVars }) => {
    templateVars.layouts.default = layoutPath('default.vue')
  })
...
```

この設定だと `admin` 側に `default.vue` を置いても反映されないので、運用によっては以下のように別の名前にして、塗り替えないようにするのも良いかもしれません。

```diff:packages/base/module.js
...
  this.nuxt.hook('build:templates', ({ templateVars }) => {
-    templateVars.layouts.default = layoutPath('default.vue')
+    templateVars.layouts.base_default = layoutPath('default.vue')
  })
...
```

```packages/admin/pages/index.vue
<template>
  <base-button />
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  layout: 'base_default',
})
</script>
```

## static の共有

`base/static` に置いたファイルが適応されます。同じファイル名であれば、継承先が優先されます。


## CSS の共有

CSS はプロジェクトごとにCSSファイルを作って、CSSの中で `base` の CSS を `@import` するようにします（試したところ、`@nuxtjs/style-resources` を使う場合はそうしかできませんでした）。

まずは Sass の導入から。変数を Vue ファイルで使いたいので `@nuxtjs/style-resources` を入れます。

```
yarn add -D -W sass-loader node-sass @nuxtjs/style-resources
```

ベースプロジェクトに適当な scss ファイルを作成します。

```packages/base/assets/scss/foundation.scss
$crimson: crimson;

body {
  background: lightsalmon;
}
```

CSS の設定を `nuxt.config.js` に記述します。

```diff:packages/base/nuxt.config.js
{
  ...
- css: [],
+ css: ['~/assets/scss/foundation.scss'],
  ...,
- modules: [],
+ modules: ['@nuxtjs/style-resources'],
  ...,
+ styleResources: {
+   scss: ['~/assets/scss/*.scss'],
+ },
}
```

admin の方で CSS を import します。

```packages/admin/assets/scss/foundation.scss
@import '~base/assets/scss/foundation.scss';
```

`nuxt.config.js` は以下のようになります。

```diff:packages/admin/nuxt.config.js
import defaultConfig from 'base/nuxt.config'

export default {
  ...defaultConfig,
  head: {
    title: 'admin',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: '' },
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
  },
+ css: ['~/assets/scss/foundation.scss'],
- modules: ['base/module'],
+ modules: [...defaultConfig.modules, 'base/module'],
}
```

`$crimson` が使えるようになっています。

```packages/admin/pages/index.vue
<template>
  <div class="hoge">ほげ</div>
</template>

<style lang="scss">
.hoge {
  color: $crimson;
}
</style>
```


## 画像の共有

`base` の `assets` の画像を `admin` で使いたいときは以下のようにすれば良いです。

```packages/admin/pages/index.vue
<template>
  <div>
    <img src="~base/assets/images/hoge.jpg" alt="" />
  </div>
</template>
```

## middleware, plugins, store の共有

これらを `admin` 側にファイルを作成せずに組み込みたい場合は、[Nuxt Module を作成してロジックを共有する方法が書かれたこちらの記事](https://medium.com/carepenny/creating-a-nuxt-module-1c6e3cdf1037)が参考になります。

今回はその方法は使わず、シンプルに `.ts` ファイルを作成して `base` 側のファイルを読み込む方法をとります。

### middleware の共有

```packages/base/middleware/log.ts
import { Middleware } from '@nuxt/types'

const middleware: Middleware = ({ route }) => {
  console.log(route.path)
}

export default middleware
```

```packages/admin/middleware/log.ts
export { default } from 'base/middleware/log'
```


### plugins の共有

```packages/base/plugins/log.ts
import { Plugin } from '@nuxt/types'

const plugin: Plugin = (_, inject) => {
  inject('log', () => {
    console.log('Hello from plugin!')
  })
}

export default plugin
```

```packages/admin/plugins/log.ts
export { default } from 'base/plugins/log'
```

inject したプラグインは、型定義を追加することを忘れずに。

```types/nuxt/index.d.ts
import Vue from 'vue'

declare module 'vue/types/vue' {
  interface Vue {
    $log: () => void
  }
}

declare module 'vuex' {
  interface Store<S> {
    $log: () => void
  }
}
```

```diff:packages/base/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "baseUrl": ".",
    "types": [
      "@types/jest",
+     "../../types/nuxt"
    ]
  }
}
```

```diff:packages/admin/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "baseUrl": ".",
    "types": [
      "@types/jest",
+     "../../types/nuxt"
    ]
  }
}
```

### store の共有

```packages/base/store/simple.ts
import { GetterTree, MutationTree } from 'vuex'

interface State {
  text: string
}

export const state: () => State = () => ({
  text: '',
})

export const getters: GetterTree<State, {}> = {
  text: ({ text }) => text,
}

export const mutations: MutationTree<State> = {
  setText(currentState, { text }) {
    currentState.text = text
  },
}
```

```packages/admin/store/simple.ts
export * from 'base/store/simple'
```

## Composition API ロジックの共有

切り出したロジックを共有する場合です。こちらはそのまま `base` にあるファイルを参照できます。`@vue/composition-api` のインストールなどは省略します。（ちなみに `@nuxtjs/composition-api` を使わないのは `@vue/composition-api` に依存していて、`@vue/composition-api` 側が変更されたら追従するまで動かなくなったりして辛かったからです。。）

```packages/base/compositions/index.ts
import { reactive, toRefs } from '@vue/composition-api'

export const useText = () => {
  const state = reactive({
    text: '0',
  })

  const changeText = () => {
    state.text = `${Math.random()}`
  }

  return {
    changeText,
    ...toRefs(state),
  }
}
```

```packages/admin/components/SuperButton.vue
<template>
  <button class="b-btn" @click="changeText">{{ text }}</button>
</template>

<script lang="ts">
import { defineComponent } from '@vue/composition-api'
import { useText } from 'base/compositions'

export default defineComponent({
  setup() {
    const { text, changeText } = useText()

    return {
      text,
      changeText,
    }
  },
})
</script>
```

## 運用できるか

~~まだ、これから新しく作るプロジェクトと、既存プロジェクトの改修に適用しようとしている段階なので、なんとも言えません。~~
[追記：使ってみました（2021-02-15）](#追記2021-02-15)

単純に monorepo 特有の問題がいろいろありそうです。今はまだサービスが初期段階なので、まとめてリリースでも問題なさそうですが、リリース周りをうまくやらないとなあという印象があります。

[monorepoからmanyrepoへ移行した記事](https://blog.spacemarket.com/code/web-frontend-repository-composition-monorepo-or-manyrepo/)には以下のような問題点が挙げられていたり

> - 共通処理パッケージのバージョン管理ができない
>   - 他サービスで更新したが、まだそれを適用したくないサービスも存在する
>   - サービスが増えるごとに影響範囲が大きく、共通処理パッケージの変更が怖くなり、あまり共通化しなくなることも……（テストが充実すればある程度怖さはなくなるかも？）
> - コミット履歴としては1つのため、1サービスに閉じた更新も全サービスの更新に近く、不安感がある
>   - Aというサービスの更新をして、次にBの更新をすると、パッケージは別れているものの、コミット履歴的にBはAの更新により影響を受けていないか不安になる

[Git をこねくりまわして monorepo のリリースを頑張ってるこの記事](https://quipper.hatenablog.com/entry/2019/04/02/quipper-web-git-flow-on-monorepo)みたいに、ゆくゆくはリリースを分割できるようにしないといけない気がしてます。

何かしら課題が出てくるかもしれないので、何かあれば気が向いたら更新します。

## 追記（2021-02-15）

進捗を報告します。

### 実際の構成

Firebase(Hosting, Functions, ...) + Github Actions というデプロイ構成です。dev, stg, pro 環境それぞれ Firebase Project を作ってそれを切り替えるかたちにしてます。

lerna で管理している packages は以下です。

- base: 共通で使うファイル郡
- user: エンドユーザー用ビュー
- admin: 管理画面用ビュー
- super: 管理者を管理する画面用ビュー
- functions: Cloud Functions のファイル

### デプロイ

問題なくできています、master プッシュ発火の Github Actions のファイルは以下です。

```.github/workflows/ci.yml
name: ci

on:
  push:
    branches:
      - main
      - master

jobs:
  ci:
    runs-on: ${{ matrix.os }}

    strategy:
      matrix:
        os: [ubuntu-latest]
        node: [12]

    steps:
      - name: Checkout 🛎
        uses: actions/checkout@master

      - name: Setup node env 🏗
        uses: actions/setup-node@v2.1.2
        with:
          node-version: ${{ matrix.node }}

      - name: Setup env 🏗
        run: |
          echo "FIREBASE_API_KEY=XXXXXXXXXXXXXXXXXXXXXX" >> $GITHUB_ENV
          echo "FIREBASE_AUTH_DOMAIN=xxxxxxxxxxxx.firebaseapp.com" >> $GITHUB_ENV
          echo "FIREBASE_DATABASE_URL=https://xxxxxxxxxxxx.firebaseio.com" >> $GITHUB_ENV
          echo "FIREBASE_PROJECT_ID=xxxxxxxxxxx" >> $GITHUB_ENV
          echo "FIREBASE_STORAGE_BUCKET=xxxxxxxxxxx.appspot.com" >> $GITHUB_ENV
          echo "FIREBASE_MESSAGING_SENDER_ID=XXXXXXXXXXXXX" >> $GITHUB_ENV
          echo "FIREBASE_APP_ID=xxxxxxxxxxxxxxxxxxxxxxxxxx" >> $GITHUB_ENV

      - name: Get yarn cache directory path 🛠
        id: yarn-cache-dir-path
        run: echo "::set-output name=dir::$(yarn cache dir)"

      - name: Cache node_modules 📦
        uses: actions/cache@v2
        id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
        with:
          path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-yarn-

      - name: Install dependencies 👨🏻‍💻
        run: yarn

      - name: Run linter 👀
        run: yarn lerna run lint

      - name: Run tests 🧪
        run: yarn test

      - name: Run build 📦
        run: yarn lerna run build

      - name: Run generate 🏭
        run: yarn lerna run generate

      - name: Run deploy 🚀
        run: yarn deploy:dev --token=${{ secrets.FIREBASE_TOKEN }}
```

functions だけ、他と同じノリで monorepo だからと dependencies から依存を削除するとデプロイでコケるので注意です。

### 依存の更新（dependabot）

create-nuxt-app で初期設定されたバージョンから dependabot でガンガンアップデートしましたが、ほぼ問題なかったです。

`sass-loader@11.0.1` だけ更新に失敗しましたが、こちらはこの構成だからというより[Vueとの組み合わせによる問題](https://stackoverflow.com/questions/66082397/typeerror-this-getoptions-is-not-a-function)のようです。 10系で止めるかたちで対処しています。

### 気になった点・問題点

以下3点です。すべて慣れれば問題なく運用できましたが、 `tailwindcss` が必須な人は頑張らないといけないかもです。

- @nuxtjs/tailwindcss が使えない（設定頑張ったら使える？）
    - purge の対象ファイル設定が単体プロジェクト前提なので、baseで利用しているクラス名がないものとして扱われます
    - `nuxt.hook` とか駆使したらもしかしたら設定できるかもです
    - 僕の場合、そもそも @nuxtjs/tailwindcss ほとんど使ってなかったのでライブラリごと削除しました
- パッケージをまたぐファイル読み込みの際は、ファイルを追加したあと Window Reload が必要
    - 定義されてないと言われる 
![スクリーンショット 2021-02-15 14.40.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94526/f5f652b3-1f7b-d856-9915-1f180937c050.png)
    - Command+Shift+P で Reload Window 
![スクリーンショット 2021-02-15 14.41.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94526/9422365f-7cd4-575d-24c8-e01494434b58.png)
- base の component で img を指定するとき `~assets/..` ってやると、その component を別パッケージで呼び出したとき動かないから `~base/assets/..` で呼び出す必要がある

## 追記（2021-06-18）

本番運用を開始しています。

ほぼ問題はなかったのですが、 `packages/base/static` に置いたファイルを引き継ぐ設定にしているにもかかわらず、各プロジェクトのビルド結果に配置されていなかったので、各プロジェクトの `static` に同ファイルを置く対応をしました。



## 参考記事

- [5 Tips for Sharing code between NuxtJS Projects](https://medium.com/dailyjs/5-tips-for-sharing-code-between-nuxtjs-projects-6ffb5b7f8a25)
- [LernaとYarn WorkspacesでMonorepo管理](https://blog.cybozu.io/entry/2020/04/21/080000)
- [lernaコマンドの標準出力が微妙な時はstreamオプションをつける](https://techblog.roxx.co.jp/entry/2020/01/20/170445)