開発規約とツールチェーン
リポジトリ: epicweb-dev/epic-stack 分析日: 2026-02-18
概要
Epic Stack の開発規約は「共有設定パッケージによる一元管理」と「Node.js 標準仕様への準拠」を軸に設計されている。ESLint・Prettier・TypeScript の設定を @epic-web/config という単一パッケージに集約し、プロジェクト側のカスタマイズを最小限に抑えている。import エイリアスは TypeScript の paths ではなく package.json の imports フィールド(Node.js 標準仕様)を採用し、ツール間の設定同期問題を根本的に排除した。ファイル命名規約(.server.ts)はフレームワーク(React Router)によるバンドル境界の enforcement と組み合わされ、実行時エラーではなくビルド時に問題を検出する仕組みになっている。
背景にある原則
設定の DRY 原則: 同じ設定を複数の設定ファイルに重複記述しない。
@epic-web/configに ESLint・Prettier・TypeScript の設定を集約し、プロジェクト側はextends/importで参照するだけにする。これにより設定間の矛盾が発生しない(根拠:eslint.config.js:1,tsconfig.json:3,package.json:162)標準仕様ファースト: ツール固有の機能より、Node.js / ECMAScript 等の標準仕様を優先する。import エイリアスを TypeScript
pathsからpackage.jsonのimportsフィールドに移行した判断がこれを象徴する(根拠:docs/decisions/046-remove-path-aliases.md,docs/decisions/031-imports.md)バリデーションの並列実行: CI パイプラインとローカル validate スクリプトの両方で lint・typecheck・test・e2e を並列実行し、フィードバックループを最短化する(根拠:
package.json:29のrun-pコマンド,.github/workflows/deploy.ymlの並列ジョブ構成)ファイル命名による境界の宣言: サーバー専用コードは
.server.tsサフィックスで明示し、フレームワークのバンドラーがクライアントバンドルへの混入を防止する。コードレビューや lint ルールではなく、ビルドツールが enforcement を担う(根拠:app/utils/配下の 17 個の.server.tsファイル)
実例と分析
共有設定パッケージ @epic-web/config による三位一体管理
@epic-web/config は ESLint・Prettier・TypeScript の設定を単一の npm パッケージとして提供する。プロジェクト側の設定ファイルは極めて薄い:
- ESLint (
eslint.config.js): 共有設定をスプレッドし、テストファイルでのreact-hooks/rules-of-hooks: offと.react-router/の ignore のみ追加 - Prettier (
package.json):"prettier": "@epic-web/config/prettier"の1行 - TypeScript (
tsconfig.json):"extends": ["@epic-web/config/typescript"]で継承し、プロジェクト固有のtypesとrootDirsのみ追加 - 型リセット (
types/reset.d.ts):@total-typescript/ts-resetを@epic-web/config/reset.d.ts経由で適用
@epic-web/config は内部で以下のプラグインを統合している:
typescript-eslint: TypeScript 固有のルールeslint-plugin-import-x: import の順序・解決eslint-plugin-react/eslint-plugin-react-hooks: React ルールeslint-plugin-testing-library/eslint-plugin-jest-dom: テストルールeslint-plugin-playwright: E2E テストルール@vitest/eslint-plugin: Vitest 固有のルール
Node.js Subpath Imports による import エイリアス
package.json の imports フィールドで2つのエイリアスを定義:
// package.json:8-11
"imports": {
"#app/*": "./app/*",
"#tests/*": "./tests/*"
}この設計は3回の意思決定を経て到達している:
- 026: TypeScript
pathsによるエイリアス → ツール間の設定同期が煩雑 - 031:
package.jsonimports+ TypeScriptpathsの併用 → TypeScript がimports未サポートだったため - 046: TypeScript
pathsを廃止しimportsのみに → TypeScript がimportsをネイティブサポート
# プレフィックスは Node.js 仕様の要件であり、通常の npm パッケージと明確に区別できる。すべてのインポートでファイル拡張子(.ts / .tsx)が明示される:
// app/routes/_auth/login.tsx:9-21
import { GeneralErrorBoundary } from "#app/components/error-boundary.tsx";
import { CheckboxField, ErrorList, Field } from "#app/components/forms.tsx";
import { login, requireAnonymous } from "#app/utils/auth.server.ts";
import { PasswordSchema, UsernameSchema } from "#app/utils/user-validation.ts";import の使い分けルール:
#app/*: アプリケーションコード間の参照(305 箇所、74 ファイル)#tests/*: テストユーティリティの参照(29 箇所、17 ファイル)- 相対パス: 同一ディレクトリまたは近接ディレクトリ内(
.server.tsと対応する.tsxの参照等) @/icon-name: shadcn/ui 互換のための特殊エイリアス(tsconfig.jsonのpathsで定義、1箇所のみ)
ファイル命名規約とバンドル境界
.server.ts サフィックスは React Router(旧 Remix)のバンドラーが認識する規約で、クライアントバンドルからの自動除外を保証する:
app/utils/
├── auth.server.ts # サーバー専用(セッション・認証)
├── cache.server.ts # サーバー専用(キャッシュ)
├── db.server.ts # サーバー専用(Prisma クライアント)
├── email.server.ts # サーバー専用(メール送信)
├── env.server.ts # サーバー専用(環境変数バリデーション)
├── misc.tsx # 共有(クライアント・サーバー両方)
├── user-validation.ts # 共有(Zod スキーマ)
└── user.ts # 共有(ユーティリティ)26 個の .server.ts ファイルが存在し、.client.ts は 0 個。これはサーバーファーストアーキテクチャを反映している。共有コードはサフィックスなし(.ts / .tsx)で、デフォルトで両環境から参照可能。
validate スクリプトの並列実行設計
// package.json:29
"validate": "run-p \"test -- --run\" lint typecheck test:e2e:run"npm-run-all の run-p で4つのタスクを並列実行:
test -- --run: Vitest のワンショット実行lint: ESLinttypecheck:react-router typegen && tsc(型生成後に型チェック)test:e2e:run: Playwright E2E テスト(pretest:e2e:runで事前にビルド)
CI ではこれがさらに分割され、4 つの独立ジョブ(lint, typecheck, vitest, playwright)として並列実行される。デプロイジョブはすべてのジョブ完了を needs で待機する。
import 順序の一貫性
コードベース全体で以下の import 順序が一貫している:
- 外部パッケージ(
@conform-to/react,react,zod等) #app/*エイリアス(コンポーネント → ユーティリティの順)- 相対パス(
./+types/,./login.server.ts等)
この順序は @epic-web/config 内の eslint-plugin-import-x によって自動 enforcement される。
コード例
// eslint.config.js:1-14
import { default as defaultConfig } from "@epic-web/config/eslint";
/** @type {import("eslint").Linter.Config} */
export default [
...defaultConfig,
// add custom config objects here:
{
files: ["**/tests/**/*.ts"],
rules: { "react-hooks/rules-of-hooks": "off" },
},
{
ignores: [".react-router/*"],
},
];// app/utils/env.server.ts:1-48
import { z } from "zod";
const schema = z.object({
NODE_ENV: z.enum(["production", "development", "test"] as const),
DATABASE_PATH: z.string(),
DATABASE_URL: z.string(),
SESSION_SECRET: z.string(),
// ...
});
declare global {
namespace NodeJS {
interface ProcessEnv extends z.infer<typeof schema> {}
}
}
export function init() {
const parsed = schema.safeParse(process.env);
if (parsed.success === false) {
console.error(
"❌ Invalid environment variables:",
parsed.error.flatten().fieldErrors,
);
throw new Error("Invalid environment variables");
}
}// tests/setup/setup-test-env.ts:1-37
import "dotenv/config";
import "./db-setup.ts";
import "#app/utils/env.server.ts";
// we need these to be imported first 👆
import { server } from "#tests/mocks/index.ts";
import { cleanup } from "@testing-library/react";
import { afterEach, beforeEach, type MockInstance, vi } from "vitest";
import "./custom-matchers.ts";
afterEach(() => server.resetHandlers());
afterEach(() => cleanup());
export let consoleError: MockInstance<(typeof console)["error"]>;
beforeEach(() => {
const originalConsoleError = console.error;
consoleError = vi.spyOn(console, "error");
consoleError.mockImplementation(
(...args: Parameters<typeof console.error>) => {
originalConsoleError(...args);
throw new Error(
"Console error was called. Call consoleError.mockImplementation(() => {}) if this is expected.",
);
},
);
// ...
});Good Patterns
- 共有設定パッケージによる設定の一元管理: ESLint・Prettier・TypeScript の設定を
@epic-web/configに集約し、プロジェクト側の設定ファイルを 5 行以下に抑えている。設定の更新は npm パッケージのバージョンアップのみで完了する。
// eslint.config.js — プロジェクト固有のオーバーライドのみ
import { default as defaultConfig } from "@epic-web/config/eslint";
export default [
...defaultConfig,
{ files: ["**/tests/**/*.ts"], rules: { "react-hooks/rules-of-hooks": "off" } },
];- Zod による環境変数のバリデーションと型安全性:
app/utils/env.server.tsで環境変数を Zod スキーマでバリデーションし、z.inferでProcessEnvの型を拡張する。起動時にバリデーションを実行し、不正な環境変数を即座に検出する。
// app/utils/env.server.ts:31-34
declare global {
namespace NodeJS {
interface ProcessEnv extends z.infer<typeof schema> {}
}
}- console.error のテスト時 throw 化:
setup-test-env.tsでconsole.errorとconsole.warnを throw するように mock し、意図しない警告やエラーをテスト失敗として捕捉する。意図的な呼び出しはconsoleError.mockImplementation(() => {})で明示的に許可する。
// tests/setup/setup-test-env.ts:18-27
consoleError = vi.spyOn(console, "error");
consoleError.mockImplementation(
(...args: Parameters<typeof console.error>) => {
originalConsoleError(...args);
throw new Error(
"Console error was called. Call consoleError.mockImplementation(() => {}) if this is expected.",
);
},
);- ADR(Architecture Decision Records)による意思決定の記録:
docs/decisions/に 47 個の ADR を蓄積し、「なぜその選択をしたか」を追跡可能にしている。import エイリアスだけでも 3 つの ADR(026 → 031 → 046)が残されており、意思決定の変遷が明確。
Anti-Patterns / 注意点
- 共有設定パッケージへの過度な依存:
@epic-web/configのようなブラックボックス化された設定パッケージは、内部のルールを把握しにくくなる。プロジェクト固有のルール追加時に共有設定と競合する場合、デバッグが困難になる。
// Bad: 共有設定の内容を確認せずルールを追加
export default [
...defaultConfig,
{ rules: { "import/order": ["error", {/* custom config */}] } }, // 共有設定と競合する可能性
];
// Better: 共有設定の内容を確認し、上書きする理由をコメントで明示
export default [
...defaultConfig,
{
// @epic-web/config の import/order は alphabetical だが、
// このプロジェクトではモジュール種別でグループ化したい
rules: { "import-x/order": ["error", { groups: [/* ... */] }] },
},
];- ファイル命名規約の暗黙知:
.server.tsがクライアントバンドルから除外されることはフレームワーク(React Router)の知識を前提とする。新規参加者がサフィックスなしでサーバー専用コードを書くと、秘匿情報がクライアントに漏洩するリスクがある。
// Bad: サフィックスなしでサーバー専用コードを記述
// app/utils/secrets.ts — クライアントバンドルに含まれる可能性
export const API_KEY = process.env.SECRET_API_KEY;
// Better: .server.ts サフィックスで明示
// app/utils/secrets.server.ts — フレームワークが除外を保証
export const API_KEY = process.env.SECRET_API_KEY;導出ルール
[MUST]コード品質ツール(ESLint・Prettier・TypeScript)の設定は共有設定パッケージまたはプリセットを使い、プロジェクト側は差分のみカスタマイズする- 根拠: Epic Stack は
@epic-web/configで3ツールの設定を集約し、プロジェクト側の設定ファイルを最小限に抑えている(eslint.config.jsは 14 行、tsconfig.jsonは 15 行)
- 根拠: Epic Stack は
[MUST]import エイリアスはtsconfig.jsonのpathsではなく、package.jsonのimportsフィールド(Node.js Subpath Imports)を使用する- 根拠:
pathsは TypeScript 固有の機能で、Vitest・ESLint 等の各ツールに個別のパス解決設定が必要。importsは Node.js 標準仕様のためツール横断で自動解決される(docs/decisions/046-remove-path-aliases.md)
- 根拠:
[MUST]CI パイプラインでは lint・型チェック・単体テスト・E2E テストを独立したジョブとして並列実行し、デプロイはすべての完了を待機する- 根拠: Epic Stack の
deploy.ymlは lint, typecheck, vitest, playwright を並列ジョブで実行し、deploy ジョブがneeds: [lint, typecheck, vitest, playwright, container]で全完了を待機する
- 根拠: Epic Stack の
[SHOULD]サーバー専用コードはファイル名サフィックス(.server.ts)で宣言し、バンドラーによるクライアント除外を活用する- 根拠: Epic Stack は 26 個の
.server.tsファイルでサーバー専用コードを分離し、React Router のバンドラーがクライアントへの混入を防止する
- 根拠: Epic Stack は 26 個の
[SHOULD]import パスにはファイル拡張子(.ts/.tsx)を明示する- 根拠: Epic Stack の全 import(305 箇所以上)で拡張子が明示されており、ESM 環境での解決の明確性とエディタの補完精度が向上する
[SHOULD]環境変数は Zod スキーマでバリデーションし、z.inferで型安全性を確保する。アプリケーション起動時にバリデーションを実行する- 根拠:
app/utils/env.server.tsで起動時バリデーションとProcessEnv型拡張を実装し、不正な環境変数を実行時ではなく起動時に検出する
- 根拠:
[SHOULD]テスト環境でconsole.error/console.warnを throw に変換し、意図しない警告を見逃さない- 根拠:
tests/setup/setup-test-env.tsで実装。意図的な呼び出しはconsoleError.mockImplementation(() => {})で明示的にオプトインする設計
- 根拠:
[SHOULD]設計上の意思決定は ADR(Architecture Decision Records)として記録し、「なぜその選択をしたか」を追跡可能にする- 根拠: Epic Stack は
docs/decisions/に 47 個の ADR を蓄積し、import 戦略の3回の変遷(026 → 031 → 046)も含め、意思決定の根拠を保持している
- 根拠: Epic Stack は
[AVOID]ツール固有のパス解決設定(TypeScriptpaths, Vitestresolve.alias, JestmoduleNameMapper)を複数の設定ファイルに重複定義すること- 根拠: Epic Stack は
tsconfig.jsonのpathsを廃止しpackage.jsonimportsに一元化することで、設定の同期問題を根本的に排除した(docs/decisions/046-remove-path-aliases.md)
- 根拠: Epic Stack は
適用チェックリスト
- [ ] ESLint・Prettier・TypeScript の設定が共有プリセット(
@epic-web/config,eslint-config-*等)をベースにしており、プロジェクト固有のオーバーライドが最小限か - [ ] import エイリアスが
package.jsonのimportsフィールドで定義されているか(tsconfig.jsonのpathsと重複していないか) - [ ] サーバー専用コードがファイル名規約(
.server.ts等)またはディレクトリ規約で明確に分離されているか - [ ] validate / CI スクリプトで lint・型チェック・テスト・E2E が並列実行されているか
- [ ] 環境変数が Zod 等のスキーマバリデーションで型安全に管理されているか
- [ ] テスト環境で
console.error/console.warnの意図しない呼び出しが検出される仕組みがあるか - [ ] 重要な設計判断(ツール選定・規約変更等)が ADR やドキュメントとして記録されているか
- [ ] import パスにファイル拡張子が明示されているか(ESM プロジェクトの場合)