コンテンツにスキップ
svelte-vitals
日本語
Esc
移動開く⌘Jプレビュー
このページの内容

architecture/reserved-directory-names · 予約ディレクトリ名

ディレクトリの直下に置ける名前は、その位置に宣言した名前だけにすべきです。

重大度: info · カテゴリ: architecture

チェック内容

ディレクトリの名前が、その位置について宣言した名前のいずれとも一致していない箇所を検出します。 コンポーネントユニットの中に、本来 parts/functions/tests/ しか置けないはずの helpers/ がある、といったケースです。

このルールは設定するまで無効です。プロジェクトがどの名前を予約しているかについて、既定の考えを 持ちません。

なぜ重要か

閉じたディレクトリ名の集合は、閉じ続けて初めて書き留める価値があります。そこから外れる最初の ディレクトリは何も困りません。記法は正しいし、置き場所ももっともらしい。しかしその表はツリーを 説明するものではなくなります。それ以降、1 つの例外に出会った読み手は、そのディレクトリが何を持っているかを 知るために毎回中を開かなければならなくなります。

architecture/directory-naming はディレクトリの記法を検査しますが、このルールはその名前を 検査します。helpers/ は全体が camelCase なので、記法の宣言はこれに何も異議を唱えません。

修正方法

ディレクトリを宣言済みの名前にリネームするか、そのいずれかの下に移動するか、その名前を宣言に 追加してください。集合を広げると決めるのも正当な結論です。それが決定である限りは。

設定

オプション デフォルト
scopes ディレクトリ glob → 直下に置ける名前 のマップ {}
unitScopes 起点 glob → 直下に置ける名前 のマップ(名前が A–Z で始まるユニット用) {}
anyCaseUnitScopes 起点 glob → 直下に置ける名前 のマップ(大文字小文字を問わないユニット用) {}
exclude ディレクトリ glob のリスト []
export default {
  rules: {
    'architecture/reserved-directory-names': {
      options: {
        scopes: { 'src/lib': 'api|components|features|effect|db' },
        unitScopes: { 'src/**': 'parts|functions|stores|types|tests|styleGuide' },
        anyCaseUnitScopes: { 'src/**': 'functions|stores|types|tests' }
      }
    }
  }
};

スコープマップはキーが何を指すかが違う

scopes のキーは親を直接指します。 'src/lib'src/lib にマッチし、その直下のサブ ディレクトリが取り得る名前は、あなたが列挙したものになります。

unitScopes のキーは起点を指します。 'src/**'src 配下のすべてのディレクトリにマッチし、 このルールはそのうちユニットであるものの子を対象にします。ユニットとは、名前が大文字で始まり、 自分の名前を冠したファイルを持つディレクトリです(Card/Card.svelteCard/Card.tsCard/Card.svelte.ts)。glob が届かない対象、つまり任意の深さで入れ子になりうるユニットに対する 閉じた集合には、これを使ってください。

anyCaseUnitScopes のキーも起点を指しますが、大文字小文字を問わないユニットを対象にします。 文字の条件を除いた同じ判定で、.ts.svelte.ts を entry とするユニット (formatDate/formatDate.tsuseThing/useThing.svelte.ts)も数えます。unitScopes の文字条件は 小文字のユニットを除外するため、このオプションがなければ汎用のユニットマップ宣言がその子を検査する ことはありません。ただし親を直接指す scopes のキーであれば届きます。実在のツリーでは 299 ユニット中 129(43%)がこの大文字小文字を問わないユニットに該当します。どちらのユニットオプション も裸の「unit」という語では命名していません。両方の判定が存在すると、その語だけではどちらの判定を 指すのか曖昧になるためです(architecture/reserved-name-placement も同じ分割を自身のオプションに 採用しています)。

scopes のキーは、子がすべて列挙した名前から成る場合にのみ書く価値があります。ルート ディレクトリは予約名とルートセグメントを並べて持ちますが、ルートセグメントはページごとに 1 つで無制限なので、そこに宣言を置くべきではありません。それでも書けば、すべてのセグメントが報告されます。

同じことは、予約名とプロジェクトが自由に付ける名前が混在するあらゆる位置に当てはまります。自分専用の 入れ子ヘルパーを tests/ と並べて持つ camelCase のユニットがそれです。ヘルパー名はルートセグメントと 同じく無制限なので、scopes: { 'src/**/functions/*': 'tests' } と書けばそのすべてが報告されます。 つまり語彙を強制できるのは、コンポーネントユニットの下と、子が本当に閉じた一覧になっている位置だけで、 予約名が現れるすべての場所ではありません。

ある宣言の名前の集合は、別の宣言のそれと同じである必要はありません。宣言された位置ごとに独自の 閉じた集合があり、単一の表というものは存在しません。

どの宣言が優先されるか

複数のマップが 1 つのディレクトリにマッチした場合は、より特異なキーが優先されます。パスの セグメント数が多いほうが先、同数なら ** セグメントが少ないほう、それも同じならキーが長いほう、 最後に辞書順です。これによって、どのマップも他方を狭めることができます。

同一の glob だけは、この手順では分けられない組み合わせで、そこでは固定の優先順位が決めます。 scopes はどちらのユニットマップにも優先し、unitScopesanyCaseUnitScopes に優先します。

scopes がユニットマップに優先するのは、scopes がキーのマッチするすべてのディレクトリに適用される のに対し、ユニットマップはそれぞれが求める大文字小文字のユニットにしか適用されないからです。同じ glob を scopes とユニットマップの両方に宣言すると報告されます。両方が宣言されている範囲では scopes が優先されるため、そこではユニットマップ側のエントリは何もチェックしません。(それ以外の 場所ではなお適用され得ます。たとえばグローバルに宣言した unitScopes のキーが、overrides エントリの中だけで追加された scopes のキーに覆われている場合、そのオーバーライドの適用範囲の外 では引き続き適用されます。)

unitScopesanyCaseUnitScopes に優先するのは、unitScopes の文字条件のほうが 2 つのゲートの うち狭いからです。大文字始まりのユニットは常に大文字小文字を問わないユニットでもありますが、逆は 成り立ちません。そのため両方のマップに同一の glob を書くと、衝突ではなく分割になります。 unitScopes は大文字始まりのユニットを対象にし、anyCaseUnitScopesunitScopes が届かない 小文字のユニットだけを単独で対象にします。両方のエントリが実際に仕事をしているため、これは無効な 宣言として報告されません。

options: {
  // 大文字始まりのユニットには parts と styleGuide も許可し、小文字のユニットには許可しない
  unitScopes: { 'src/**': 'parts|styleGuide|functions|stores|types|tests' },
  anyCaseUnitScopes: { 'src/**': 'functions|stores|types|tests' }
}

末尾/** は「このディレクトリ配下すべて」を意味し、ディレクトリ自身は対象にしません。

exclude

exclude はそのディレクトリと配下すべてを対象外にします。 名前を自分でコントロールできない サブツリーに使ってください。

options: {
  unitScopes: { 'src/**': 'parts|functions|tests' },
  exclude: ['**/generated']
}

制限

対象は src/ 配下のディレクトリだけです。ファイル名は検査されません。ドットディレクトリは 決して現れないため、除外する必要もありません。

名前が大文字で始まるが、自分の名前を冠したファイルを持たないディレクトリは、ここではユニットでは なく、このルールはその子について何も言いません。そのディレクトリは architecture/unit-entry-file の finding です。子ごとにではなく、1 回だけ報告されます。

「同名のファイル」の判定は、ディレクトリ名とファイル名の最初のドットより前を比較します。 これが Card/Card.svelte.ts を通すための条件です。帰結として、同名のファイルがテストだけの ディレクトリ(Card/Card.test.ts)もユニットとして扱われ、その子が検査されます。拡張子 1 つ分を 除く方式では Card.svelte.ts という実在の形を弾いてしまうため、読者が却下できる finding が出る ほうを軽い失敗として選んでいます。

このカットが適用されるのはファイル名側だけで、ディレクトリ名側はカットせず全体を比較します。 そのため src/lib/Card.v2/ は、Card.v2.svelte を持っていてもこのルールではユニットになりません。 そのファイル名の最初のドットまでの語幹は Card であり、ディレクトリ自身のカットしていない名前 Card.v2 とは一致しないからです。その子は検査されません。architecture/unit-entry-file は明示的な拡張子で 設定した場合、別の問い、つまり Card.v2 + .svelte が存在するかどうかを立てます。そして同じ ディレクトリに対して「はい」と答えます。どちらの答えも、それぞれのルール自身の定義とは矛盾しません。

このルールが言うのは「ここでは、これらの名前だけ」であり、「この名前は、ここだけ」とは言えません。 間違った場所にある parts/ は、その場所自体が宣言されていない限り見えないままです。

ユニットの直下にユニットを入れ子にするプロジェクトは unitScopesanyCaseUnitScopes も 宣言すべきではありません。入れ子になったユニットは集合に含まれない子となり、報告されてしまいます。

宣言が書いてある内容を実際には検査していない場合は報告されるので、書き間違いでルールが黙って何も しない状態にはなりません。次のケースがこの finding に該当し、それぞれメッセージに名前が出ます。

  • glob が 1 つのディレクトリにもマッチしなかった
  • マッチしたディレクトリがすべて除外されていた
  • unitScopes のキーがディレクトリにはマッチしたがユニットには一度もマッチしなかった
  • anyCaseUnitScopes のキーがディレクトリにはマッチしたが、大文字小文字を問わずユニットに一度も マッチしなかった。大文字始まりのユニットは常に大文字小文字を問わないユニットでもあるため、 こちらのほうが強い主張です
  • 値が名前を 1 つも列挙していなかった
  • 同じ glob が scopes とユニットマップの両方に宣言されており、双方の値が 1 つ以上の名前を 挙げていた。どちらかの値が何も挙げていない場合はマッチ前に捨てられ、もう一方が単独で適用される ため、代わりに「値が名前を 1 つも列挙していない」として報告されます。2 つのユニットマップの 両方に同じ glob を宣言した場合はこのケースに当たりません。上の「どの宣言が優先されるか」を 参照してください。

意図的にまったく報告されないものが 2 つあります。

  • overrides エントリの中だけで宣言したキー。何にマッチしたかがそのオーバーライドの適用範囲に 依存するためです。ただし 1 つ例外があり、同一 glob の衝突検査はグローバル宣言に絞られていないため、 overrides だけから組み上がった scopes とユニットマップの衝突は報告されます。
  • 現在どのディレクトリも使っていない宣言済みの名前。この集合が表すのは現れてよいものであって、 出現を義務付けるものではないからです。

モードによる違い

ありません。このルールが読むのは、ファイル内容ではなくプロジェクトのソースファイル一覧(src/** のパス)です。この一覧は CLI、Vite プラグインのビルド、ライブダッシュボードの静的ベースラインのいずれでも同じように作られるため結果は同一で、レンダリング済み HTML の解析で再評価されることもありません。--route で実行範囲を絞ると、このルールは動きません。一覧が作られず、ファイルの検出には紐づけるルートも無いためです。

無効化

個別に抑制するには、対象行の直前に <!-- svelte-vitals-disable-next-line architecture/reserved-directory-names --> を置きます。ルールごと無効化するには、次のように設定します。

export default {
  rules: {
    'architecture/reserved-directory-names': 'off'
  }
};