◆ 各プラグインがどうやって設定を参照するか

設定を持つ場所

Fastify CLI を使うときに設定オブジェクトをどうやってもつのがいいか考えてます
設定系なので dotenv や それに近い Node.js の --env-file に任せて process.env に保持という方法もあるのですが env ファイルってシンプルな構文ですし JavaScript で制御できません
分岐したり 今のファイルをベースに相対パスで指定したりしたいケースがあるので .js ファイルの方がいいです
なので .js ファイルで管理していて デフォルトの設定ファイルと それをベースに 環境変数等によって決まるファイルを差分適用する仕組みを作ってます

全ファイルでインポートはしたくない

ただ この方法だと 設定を参照するには settings.js を import して それに含まれるデータを参照する感じです
設定を参照したい箇所は多いのでほとんどのファイルで settings.js をインポートすることになります
そのやり方でも別に悪くはないのですが できるだけ設定は引数等で受け取りたい気持ちがあります

テストする場合は settings.js をモックに置き換えたり settings.js を使う場合でも環境をテストとして別の設定になるようできるので その点では困らないですが 引数だけで扱えるほうが楽ですしスッキリとしています

特に Fastify はプラグインの仕組み的にテストしやすくなっていて モジュールのモックみたいな高度な機能は不要で Node.js 組み込みの node --test だけで十分なほどの作りです
Fastify を使うならできるだけそれに合わせたいです

settings.js を各所でインポートせず Fastify の仕組みで受け取るなら 方法はこれくらいかと思います

  • plugins のプラグインで decorate
  • app.js で decorate
  • app.js で opts に入れる
  • app.js で options に入れる

プラグイン化

1 つめの方法は普通にプラグイン化するものです
プラグインの一つとして decorate を実行します
プラグインなので その他の機能と近い形で管理できます

[settings-plugin.js]
import settings from "./settings.js"
import fp from "fastify-plugin"

export default fp(async (fastify) => {
fastify.decorate("settings", settings)
})

プラグイン指向としてはこれでいいのかもと思いましたが 不便な点もありました
設定は他の多くのプラグインから参照したいです
それらのプラグインの初期化処理時に fastify.settings を参照できる必要がありますが これを参照するには先に fastify.settings を decorate するプラグインが読み込まれている必要があります
しかし autoload なので自分で register を読み込みたい順に書けません

fastify-plugin の関数でラップするときのオプションとして dependencies を設定すると順番を制御できるのですが 設定を使うプラグインはとても多いので全部に書いていくのは少し面倒です

dependencies などは fp の引数としてこういう感じで指定するものです
autoload プラグインはこの情報から作られる依存関係のグラフを見て register しているようです

import fp from "fastify-plugin"

const plugin = async (fastify) => {
// ...
}

export default fp(plugin, {
fastify: "4.x",
decorators: {
fastify: ["plugin1", "plugin2"],
reply: ["compress"]
},
dependencies: ["plugin1-name", "plugin2-name"]
})

app.js で直接 decorate

2 つめの方法は app.js で直接 decorate します
fastify.settings の decorate だけ 他のプラグインと扱いが違って特殊な方法になりますが 設定はアプリ全体で共通で最初に登録したいものなので これはこれでありかもしれません
autoload より前に登録するので各プラグインは dependencies 不要で fastify.settings を使えます
しかし dependencies がないのに fastify.settings がある前提でアクセスするのはどうなの?という気持ちもあります

[app.js]
import settings from "./settings.js"
import AutoLoad from "@fastify/autoload"

// 略

export const options = {}

export default async function (fastify, opts) {
await fastify.decorate("settings", settings)

fastify.register(AutoLoad, {
dir: path.join(__dirname, "plugins"),
options: Object.assign({}, opts)
})

fastify.register(AutoLoad, {
dir: path.join(__dirname, "routes"),
options: Object.assign({}, opts)
})
}

app.js で opts に入れる

3 つ目の方法では opts を使います

[app.js]
import settings from "./settings.js"
import AutoLoad from "@fastify/autoload"

// 略

export const options = {}

export default async function (fastify, opts) {
opts = {
...opts,
settings,
}

fastify.register(AutoLoad, {
dir: path.join(__dirname, "plugins"),
options: Object.assign({}, opts)
})

fastify.register(AutoLoad, {
dir: path.join(__dirname, "routes"),
options: Object.assign({}, opts)
})
}

autoload では各プラグインの読み込み時に渡すオプションを設定できます
それを使って settings を渡します
fastify.settings は使いません

[plugin1.js]
import fp from "fastify-plugin"

export default fp(async (fastify, opts) => {
fastify.decorate("foo", () => {
return opts.settings.condition ? 1 : 2
})
})

すべてがプラグインという形になってるので 各ルートのハンドラーの中でも opts にアクセスできます
なので fastify.settings を使わずこのやり方にしても特に問題無いと思います
それに設定なのですからプラグインというよりは opts で渡すほうが自然かもしれません

この方法で基本は問題ないように思うのですが app.js を使う場合に必ず設定が使われてしまいます
settings.js の中で完全に設定を管理してるならこれでもいいですが 外部から渡したいケースに対応できないです
外部から opts に渡しても app.js 内で上書きされてしまいます

app.js で options に入れる

4 つめの方法は options を使います
app.js がエクスポートするオブジェクトです
Fastify CLI が使うもので --options をつけた場合に opts としてプラグインが受け取ることができます

[app.js]
import settings from "./settings.js"
import AutoLoad from "@fastify/autoload"

// 略

export const options = {
settings
}

export default async function (fastify, opts) {
// --options をつけて実行すると
// 引数の opts.settings で settings にアクセスできる

fastify.register(AutoLoad, {
dir: path.join(__dirname, "plugins"),
options: Object.assign({}, opts)
})

fastify.register(AutoLoad, {
dir: path.join(__dirname, "routes"),
options: Object.assign({}, opts)
})
}

これをすると fastify start するときに毎回 --options が必要になります
--options が無いと必要な設定が opts から受け取れないので実行時にエラーになります
なので手間が増えるだけで これなら 3 番目の方法の方がいいかなと思ってました

ですが 3 番目の方法のところで書いたような 外部から別の設定を渡して使いたい場合も考えるとこっちのほうがいいのかなという気がしてます
fastify start せずに app.js を手動で register するときは自分で opts に設定を渡せばいいですし そのときにデフォルトの設定で上書きされません

設定ファイルのパスを渡す

opts で受け取る settings を設定そのもののオブジェクトにせず 設定ファイルのパスにするのもありかもと思いました
使われない場合は app.js で固定の settings.js をインポートしなくて済みますし

Fastify CLI の start では -- のあとに書いたオプションは opts にオブジェクト形式で渡されます

yarn fastify start app.js -- --settings=settings-dev.js

を実行して app.js のプラグイン内で opts をコンソールに表示すると

{ settings: 'settings-dev.js' }

これを使って Fastify CLI を使う場合はコマンドラインから それ以外の場合はプラグインの登録時に渡す opts からファイルのパスを渡します
app.js のプラグイン内でファイルを読み取ってそれを autoload に渡します
ファイルを使いたくないケースもありそうなので 一応オブジェクトで受け取ったらそのままにします

[app.js]
import AutoLoad from "@fastify/autoload"

// 略

export const options = {}

export default async function (fastify, opts) {
if (!opts.settings) {
throw new Error("settings は必須です")
}

const settings = typeof opts.settings === "object"
? opts.settings
: await import(opts.settings).then(mod => mod.default)

fastify.register(AutoLoad, {
dir: path.join(__dirname, "plugins"),
options: Object.assign({}, opts, { settings })
})

fastify.register(AutoLoad, {
dir: path.join(__dirname, "routes"),
options: Object.assign({}, opts, { settings })
})
}

autoload されるプラグイン側では 前の方法と同じく opts.settings で設定ファイルのオブジェクトにアクセスできます

settings で受け取るものをどっちにするかは settings.js がどういう動きするか次第でしょうか
この中で使うべき設定を解決して それが返ってくるなら app.js が固定でインポートして options に入れるようにしてもいいですが そういうことをしない場合は固定でインポートが難しいのでファイルをコマンドラインオプションやプラグインとして使う側から指定してもらうのが良さそうに思います