新機能を足す作業は中央配列の編集にならない。FeatureDescriptor を1つ書き、event.config.ts の flag を倒すだけで、nav・env チェックリスト・route ガードが自動追従する。配線の真実源は event.config.ts 一箇所。
唯一の配線点 — event.config.ts
import { composeFeatures, defineEventConfig, type FeatureDescriptor } from "@event/shell";
export const eventConfig = defineEventConfig({
name: "starter",
fourSSlug: "ivs26",
features: { cms: true, sessionCard: true, surveys: true, /* … */ mcp: true },
});
export const FEATURES: FeatureDescriptor[] = [ /* 各機能の自己記述(下記) */ ];
export const features = composeFeatures(eventConfig, FEATURES);
if (features.problems.length > 0) {
// 依存違反は起動時に fail-fast(黙って壊れた nav を出さない)
throw new Error(`[event.config] feature 依存違反:\n- ${features.problems.join("\n- ")}`);
}
features.features は Partial<Record<FeatureKey, boolean>>。FeatureKey は cms / sessionCard / surveys / live / booking / networking / spatial / visuals / mailer / db / mcp の 標準キーに (string & {}) を許した union で、イベント固有機能は自由文字列で足せる。
FeatureDescriptor — 機能の自己記述
各機能は nav / 必要 env / 対応マニュアル章を1つの plain object で宣言する(package 非依存):
{
key: "surveys",
label: "サーベイ & リーダーボード",
description: "匿名アンケート + IMDb 加重ランキング(@event/feedback)",
nav: [
{ id: "surveys", group: "フィードバック", label: "サーベイ", href: "/admin/surveys", code: "SUR", order: 1 },
],
env: [
{ name: "BLOB_READ_WRITE_TOKEN", required: true, description: "回答の Blob 保存(PII を git に置かない)" },
],
manualChapters: [26, 27, 28],
requires: ["cms"], // 例: sessionCard は overlay 永続先が content のため cms に依存
}
| フィールド | 効果 |
|---|---|
key / label / description | 機能の識別と表示 |
nav?: NavItem[] | 有効時に admin nav へ自動登録 |
env?: FeatureEnvVar[] | .env.example 生成(envExampleFor)と boot 検査のチェックリスト |
manualChapters?: number[] | この章サイト(event-manual)の対応章 |
requires?: FeatureKey[] | 依存機能。無効なら problems に載り起動時 throw |
composeFeatures が合成するもの
composeFeatures(config, descriptors) は config.features[d.key] === true の descriptor だけを集め、 ComposedFeatures を返す:
nav— 有効機能のNavItem[]をflatMapで連結(→ 下記 nav registry へ)envChecklist— 有効機能の env を name で重複除去(1つでもrequiredがあれば required 扱い)problems—requires違反の列挙isEnabled(key)/enabledKeys— route ガードで使う判定
nav.ts — self-register で追従
nav は中央配列を編集しない。features.nav を registry に流し込むだけ:
import { createNavRegistry } from "@event/shell";
import { features } from "@/event.config";
export const nav = createNavRegistry([
{ id: "dash", group: "Overview", label: "ダッシュボード", href: "/admin", code: "DASH", order: 1 },
...features.nav, // ← 有効機能の nav が自動で入る
]);
flag を true にした瞬間、その機能の nav 項目が現れる。false に戻せば消える。手動配線は無い。
requireFeature 相当 — 無効機能を 404 に落とす
package は Next 非依存を保つため、@event/shell は判定(isFeatureEnabled / ComposedFeatures.isEnabled)だけを提供し、 notFound() はアプリ側で呼ぶ。route / page / action の冒頭でガードする:
// app/admin/surveys/page.tsx
import { notFound, redirect } from "next/navigation";
import { cookies } from "next/headers";
import { features } from "@/event.config";
import { adminAuth } from "@/lib/adminAuth";
export default async function SurveysPage() {
if (!features.isEnabled("surveys")) notFound(); // ① 無効機能は 404
const cookie = (await cookies()).get(adminAuth.cookieName)?.value;
if (!adminAuth.isAuthedFromValue(cookie)) redirect("/login"); // ② 未認証は /login
// …
}
ファイルが残っていても config が false なら 404 になる — 配線の真実源は config。第37章の統治原則と同じで、 middleware は境界にすぎず、route / action 自身が再検査する。
guarded action の作法
server action / route handler も同じ二段ガードを踏む。feature → auth の順で、feature 無効は 404、 未認証は 401(API)を返す:
export async function POST(req: Request) {
if (!features.isEnabled("mailer")) {
return NextResponse.json({ ok: false, error: "feature_disabled" }, { status: 404 });
}
const cookie = (await cookies()).get(adminAuth.cookieName)?.value;
if (!adminAuth.isAuthedFromValue(cookie)) {
return NextResponse.json({ ok: false, error: "unauthorized" }, { status: 401 });
}
// … 本処理(rate-limit / honeypot / persist)…
}
まとめ — 新機能追加の4ステップ
FEATURESにFeatureDescriptorを1つ足す(nav / env / manualChapters / requires を記述)eventConfig.featuresの flag をtrueにするapp/…/page.tsx(+ route/action)を作り、冒頭でfeatures.isEnabled(key)→notFound()、続けて auth- env が要るなら
.env.exampleはenvExampleFor(features)で自動生成される
中央の nav 配列も env リストも手で触らない。1機能 = descriptor 1つ + flag 1つ。