ほぼチーム共有用ですが、一般的なノウハウなので Qiita に書きます。
私事ですが、Nuxt で作っている BtoBtoC サービスで「管理画面」と「その管理者を管理する管理画面」を用意する場合、ほぼ同じ画面構成・見た目という理由からリポジトリごとコピーして必要に応じて弄る、という方法がとられていました。
当たり前ですが、共通部分のコードに変更が必要になると、同じ変更を2回(実際のところ何回もコピーしたので2回じゃ済まないんですが...)やらないといけなくなっています。
あと地味にしんどいのが dependabot のお世話ですね。少し目を離すと彼が一番熱心なコミッターになるというのはあるあるです。
これを脱するため、monorepo にして共有できるものは共有しようという試みがこの記事です(移行のコストが地味にあって実現できるかは不明ですが...)
※余談ですが、共通化には罠もあるので取り扱いには注意ですね。
クソコード動画「共通化の罠」 pic.twitter.com/MM750CNXc2
— ミノ駆動 (@MinoDriven) May 12, 2019
リポジトリ
元ネタ
https://medium.com/dailyjs/5-tips-for-sharing-code-between-nuxtjs-projects-6ffb5b7f8a25 の方法を使って、より実用的なかたちで書いたのがこの記事になります。
Nuxt プロジェクトはパッケージとして publish する必要はなく、ビルドしてデプロイできればよいです。
なので、一般的な monorepo とは違い、それぞれのパッケージをひとつにまとめて publish できるようにして lib
とかを参照するのではなく、パッケージ間でソースコードをそのまま import するやり方をとります(もはやこれを monorepo というのは謎ですが)。
構造
どういう開発体験か
プロジェクトAの開発サーバーを立ち上げてるときに、ベースプロジェクトにコンポーネントを追加すると、プロジェクトAのテンプレートHTMLですぐに使えてその変更がホットリロードされる、というとなんとなくいい感じというイメージが伝わるかなと思います。
利用ライブラリなど(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
を追加。
{
...,
+ "private": true,
+ "workspaces": [
+ "packages/*"
+ ]
}
lerna のインストール
追加しないと差分がうるさくなるので先に .gitignore
を追加しておきます。あとで create-nuxt-app で書き出されるもので上書きするのでとりあえずこれだけで OK。
node_modules
ルートの pacakge.json
に lerna
をインストール
yarn add -W -D lerna
lerna のセットアップ
--independent
設定で バージョニングが個別になります。オープンソースライブラリのような「メインとそのサブパッケージ」という構成でなければ independent で良いかと。
yarn lerna init --independent
lerna.json
が書き出されます。
yarn workspaces を使うので以下のように書き換えます。
{
"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
ルートで git 管理するので、 Version control system
は None
にします。
create-nuxt-app
で生成される dependencies, devDependencies はすべてのプロジェクトで共通なので、そのままルートにコピペして、ベースプロジェクトからは削除します。ルートの package.json に既に記載してある lerna
は残しましょう。
{
...
以下を全部ルートの 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
だけになるように移動します。ルートが以下のようになります。
base
で必要な設定をする
ファイルのExtendやコマンドの修正が必要になります。
Typescript
tsconfig.json
を親から extend します。
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": "."
}
}
あとで Jest のためにまた弄ることになりますが、Jest 無しならこれで大丈夫なはずです。
ESLint
create-nuxt-app で作成したプロジェクトは lint
スクリプトが eslint --ext .js,.vue --ignore-path .gitignore .
になっています。
.gitignore
は今はルートにあるので以下に変更します。
{
...
- "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
まず、ルートの設定を以下にします。
module.exports = {
projects: ['<rootDir>/packages/*'],
}
{
...
+ "scripts": {
+ "test:base": "vue-cli-service test:unit --projects packages/base",
+ "test": "vue-cli-service test:unit"
+ },
...
}
それぞれのプロジェクトで Extend するためのベースの設定ファイルを書きます(create-nuxt-app で書き出されたものを修正したもの)。
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
を追加します。
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
+ "types": [
+ "@types/jest",
+ ]
}
}
jest.config.js
はルートの jest.config.base.js
を Extend して名前だけつけてあげます(名前をつけないと複数プロジェクトで正しく動かなかったです)。
const config = require('../../jest.config.base')
module.exports = { name: 'base', displayName: 'Base Tests', ...config }
テストも TypeScript ファイル(*.spec.ts
)を作成する想定なので .vue
ファイルの型を追加しましょう。
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}
使わないので packages.json
から test
コマンドを削除します。
{
...
"scripts": {
...
- "lint": "yarn lint:js && yarn lint:style",
- "test": "jest"
+ "lint": "yarn lint:js && yarn lint:style"
},
...
}
コマンドは以下で実行します。
yarn test # すべてのプロジェクトをテストする場合
yarn test:base # base だけテストする場合
Vetur
Vetur はデフォルトでルートの tsconfig.json
を参照するので vetur.config.js
でプロジェクトごとに tsconfig.json
の参照先を指定する必要があります。
module.exports = {
settings: {
'vetur.useWorkspaceDependencies': true,
'vetur.experimental.templateInterpolationService': true,
},
projects: [
{
root: './packages/base',
tsconfig: './tsconfig.json',
},
],
}
ベースプロジェクトのファイルを共有するための 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 だけ修正します。
{
"name": "admin",
...
}
nuxt.config.js
は base
を流用して必要な箇所だけ変更します。 head
は一応すべて定義し直しにしてます。
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
で必要だった作業をこちらでもします。
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"types": [
"@types/jest",
]
}
}
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}
const config = require('../../jest.config.base')
module.exports = { name: 'admin', displayName: 'Admin Tests', ...config }
module.exports = {
settings: {
'vetur.useWorkspaceDependencies': true,
'vetur.experimental.templateInterpolationService': true,
},
projects: [
+ {
+ root: './packages/admin',
+ tsconfig: './tsconfig.json',
+ },
{
root: './packages/base',
tsconfig: './tsconfig.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
に以下のファイルを作成して
<template>
<div class="container">Hoge</div>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({})
</script>
module.js
の pages
にそのページのパスを記述すると、そのモジュールを読み込んだ先でページが表示できます。
...
const pages = ['pages/index.vue']
...
admin
側で nuxt.config.js
で base/module
を読み込みます。
{
...
modules: ['base/module']
...
}
開発サーバーを起動してみましょう。
yarn lerna run --stream --scope admin dev
http://localhost:3000 を開いてみるとページが閲覧できるかと思います。
components の共有
base
につくったコンポーネントを共有する例です。
<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
になっていることを確認しましょう。以下のように記述するとボタンが表示されるかと思います。
<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
で塗り替えるようになります。
...
this.nuxt.hook('build:templates', ({ templateVars }) => {
templateVars.layouts.default = layoutPath('default.vue')
})
...
この設定だと admin
側に default.vue
を置いても反映されないので、運用によっては以下のように別の名前にして、塗り替えないようにするのも良いかもしれません。
...
this.nuxt.hook('build:templates', ({ templateVars }) => {
- templateVars.layouts.default = layoutPath('default.vue')
+ templateVars.layouts.base_default = layoutPath('default.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 ファイルを作成します。
$crimson: crimson;
body {
background: lightsalmon;
}
CSS の設定を nuxt.config.js
に記述します。
{
...
- css: [],
+ css: ['~/assets/scss/foundation.scss'],
...,
- modules: [],
+ modules: ['@nuxtjs/style-resources'],
...,
+ styleResources: {
+ scss: ['~/assets/scss/*.scss'],
+ },
}
admin の方で CSS を import します。
@import '~base/assets/scss/foundation.scss';
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
が使えるようになっています。
<template>
<div class="hoge">ほげ</div>
</template>
<style lang="scss">
.hoge {
color: $crimson;
}
</style>
画像の共有
base
の assets
の画像を admin
で使いたいときは以下のようにすれば良いです。
<template>
<div>
<img src="~base/assets/images/hoge.jpg" alt="" />
</div>
</template>
middleware, plugins, store の共有
これらを admin
側にファイルを作成せずに組み込みたい場合は、Nuxt Module を作成してロジックを共有する方法が書かれたこちらの記事が参考になります。
今回はその方法は使わず、シンプルに .ts
ファイルを作成して base
側のファイルを読み込む方法をとります。
middleware の共有
import { Middleware } from '@nuxt/types'
const middleware: Middleware = ({ route }) => {
console.log(route.path)
}
export default middleware
export { default } from 'base/middleware/log'
plugins の共有
import { Plugin } from '@nuxt/types'
const plugin: Plugin = (_, inject) => {
inject('log', () => {
console.log('Hello from plugin!')
})
}
export default plugin
export { default } from 'base/plugins/log'
inject したプラグインは、型定義を追加することを忘れずに。
import Vue from 'vue'
declare module 'vue/types/vue' {
interface Vue {
$log: () => void
}
}
declare module 'vuex' {
interface Store<S> {
$log: () => void
}
}
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"types": [
"@types/jest",
+ "../../types/nuxt"
]
}
}
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"types": [
"@types/jest",
+ "../../types/nuxt"
]
}
}
store の共有
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
},
}
export * from 'base/store/simple'
Composition API ロジックの共有
切り出したロジックを共有する場合です。こちらはそのまま base
にあるファイルを参照できます。@vue/composition-api
のインストールなどは省略します。(ちなみに @nuxtjs/composition-api
を使わないのは @vue/composition-api
に依存していて、@vue/composition-api
側が変更されたら追従するまで動かなくなったりして辛かったからです。。)
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),
}
}
<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)
単純に monorepo 特有の問題がいろいろありそうです。今はまだサービスが初期段階なので、まとめてリリースでも問題なさそうですが、リリース周りをうまくやらないとなあという印象があります。
monorepoからmanyrepoへ移行した記事には以下のような問題点が挙げられていたり
- 共通処理パッケージのバージョン管理ができない
- 他サービスで更新したが、まだそれを適用したくないサービスも存在する
- サービスが増えるごとに影響範囲が大きく、共通処理パッケージの変更が怖くなり、あまり共通化しなくなることも……(テストが充実すればある程度怖さはなくなるかも?)
- コミット履歴としては1つのため、1サービスに閉じた更新も全サービスの更新に近く、不安感がある
- Aというサービスの更新をして、次にBの更新をすると、パッケージは別れているものの、コミット履歴的にBはAの更新により影響を受けていないか不安になる
Git をこねくりまわして 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 のファイルは以下です。
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との組み合わせによる問題のようです。 10系で止めるかたちで対処しています。
気になった点・問題点
以下3点です。すべて慣れれば問題なく運用できましたが、 tailwindcss
が必須な人は頑張らないといけないかもです。
- @nuxtjs/tailwindcss が使えない(設定頑張ったら使える?)
- purge の対象ファイル設定が単体プロジェクト前提なので、baseで利用しているクラス名がないものとして扱われます
-
nuxt.hook
とか駆使したらもしかしたら設定できるかもです - 僕の場合、そもそも @nuxtjs/tailwindcss ほとんど使ってなかったのでライブラリごと削除しました
- パッケージをまたぐファイル読み込みの際は、ファイルを追加したあと Window Reload が必要
- base の component で img を指定するとき
~assets/..
ってやると、その component を別パッケージで呼び出したとき動かないから~base/assets/..
で呼び出す必要がある
追記(2021-06-18)
本番運用を開始しています。
ほぼ問題はなかったのですが、 packages/base/static
に置いたファイルを引き継ぐ設定にしているにもかかわらず、各プロジェクトのビルド結果に配置されていなかったので、各プロジェクトの static
に同ファイルを置く対応をしました。