Vue3でrouterのコンフリクトを防ぐ

# 概要

[Vue3](https://v3.vuejs.org/) で `router/index.ts` が Pull Request / Marge Request のたびに毎回コンフリクトして困っていたので、`views` 以下のディレクトリ構成とファイル名から自動で `router/index.ts` が入るようにしてみました！


# 背景

[Vue3](https://v3.vuejs.org/) とても便利なのですが、[vue-cli](cli.vuejs.org) で [vue-router](next.router.vuejs.org) とともに使っていて、
`router/index.ts` のコンフリクト解決が面倒に感じています。
[Nuxt 3](https://v3.nuxtjs.org/) が使えれば解決なのですがまだ Beta 版で IntelliJ/WebStorm の Vue プラグインも対応していないのでちょっと使いづらいです。
そこで簡単に `router/index.ts` を `npm run` するときに生成してしまうのを作ってしまおうというのが今回の取り組みです。


# 目指すもの

- `router/index.ts` の内容は `npm run dev`, `npm run build` 時に自動で入り、メンテナンス不要。
- ついでに、すべてのページにデバッグ用にアクセスできる索引的なコンポーネントも生成する。


# 実装方法

### 1. メインとなるプログラムの dynamicRouterGenerator を配備

#### 1-1. `src/lib/dynamicRouterGenerator/.gitignore`

```
generated-router.ts
GeneratedIndex.vue
```

#### 1-2. `src/lib/dynamicRouterGenerator/.gitignore`

```js
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fs = require('fs')

let files = []

/**
 * @param dirPath 「/」始まり or ''
 */
const searchFiles = (dirPath) => {
  const allDirents = fs.readdirSync(`${__dirname}/../../views${dirPath}`, { withFileTypes: true })
  for (const dirent of allDirents) {
    if (dirent.isDirectory() && dirent.name !== 'components') {
      searchFiles(`${dirPath}/${dirent.name}`)
    } else if (dirent.isFile() && dirent.name.match(/\.vue$/)) {
      const importPath = `@/views${dirPath}/${dirent.name}`
      const accessPath = `${dirPath}/${dirent.name}`
        .replace(/\.vue$/, '')
        .replace(/\/index$/, '')
        || '/'
      const moduleName = `${dirPath}/${dirent.name}`
        .replace(/^\//, '')
        .replace(/\.vue$/, '')
        .replace(/\//g, '__')
        .replace(/-/g, '_')
      const title = `${dirPath}/${dirent.name}`
        .replace(/^\//, '')
        .replace(/\.vue$/, '')
        .replace(/-/g, ' ')
        .replace(/\b(\w)/g, (w) => w.toUpperCase())
        .replace(/\//g, '<span style="margin-left: 40px;"></span>')
      files.push({
        accessPath,
        importPath,
        moduleName,
        title,
      })
    }
  }
}

searchFiles('')

files.sort((a, b) => a.accessPath < b.accessPath ? -1 : 0)

fs.writeFileSync(`${__dirname}/generated-router.ts`, `\
import { RouteRecordRaw } from 'vue-router'
${files.map(f => `\
import ${f.moduleName} from '${f.importPath}'
`).join('')}\

export const dynamicRoutes: Array<RouteRecordRaw> = [
${files.map(f => `\
  { path: '${f.accessPath}', component: ${f.moduleName} },
`).join('')}\
]
`)


fs.writeFileSync(`${__dirname}/GeneratedIndex.vue`, `\
<template>
  <ul>
${files.map(f => `\
    <li><router-link to="${f.accessPath}">${f.title}</router-link></li>
`).join('')}\
  </ul>
</template>
`)
```

### 2. router 書き換え

#### 2-1. `src/router/index.ts`

```ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { dynamicRoutes } from '@/lib/dynamicRouterGenerator/generated-router'

const routes: Array<RouteRecordRaw> = [
  ...dynamicRoutes,
]

const index = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default index
```

### 3. 索引ページ配置

#### 3-1. `src/views/index-for-debug.vue`

```vue
<template>
  <GeneratedIndex style="font-size: 40px;" />
</template>

<script setup lang="ts">
import GeneratedIndex from '@/lib/dynamicRouterGenerator/GeneratedIndex.vue'
</script>

<style scoped>
li { font-size: 40px; }
</style>
```

### 4. package.json

#### 4-1. `npm install`

```
npm install --save-dev @types/node
```

#### 4-2. `scripts`

```:元
  "scripts": {
    "dev": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "test:unit": "vue-cli-service test:unit",
    "lint": "vue-cli-service lint"
  },
```

↓

```:変更後
  "scripts": {
    "dev": "node src/lib/dynamicRouterGenerator && vue-cli-service serve",
    "build": "node src/lib/dynamicRouterGenerator && vue-cli-service build",
    "test:unit": "node src/lib/dynamicRouterGenerator && vue-cli-service test:unit",
    "lint": "node src/lib/dynamicRouterGenerator && vue-cli-service lint"
  },
```

# 使い方

通常通り `npm run dev` や `npm run build` するだけ。
必要なファイルが自動で入ります。
新規ページを生成したときは `npm run dev` を再起動します。

http://localhost:8080/index-for-debug
にアクセスすることで索引も表示されます。

```
src/
  views/
    index.vue
    aaa/
      bbb/
        ccc-ddd.vue
        ccc-eee.vue
```

という構成の場合、下記のような索引ページが生成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29899/e420c9e2-26f5-a3e1-7d51-4cab1777e1ca.png)