ガイド 第42

新機能の追加

page + nav.ts + guarded action のフルフロー

@event/shell

新機能を足す作業は中央配列の編集にならない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.featuresPartial<Record<FeatureKey, boolean>>FeatureKeycms / 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 扱い)
  • problemsrequires 違反の列挙
  • isEnabled(key) / enabledKeys — route ガードで使う判定

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ステップ

  1. FEATURESFeatureDescriptor を1つ足す(nav / env / manualChapters / requires を記述)
  2. eventConfig.features の flag を true にする
  3. app/…/page.tsx(+ route/action)を作り、冒頭で features.isEnabled(key)notFound()、続けて auth
  4. env が要るなら .env.exampleenvExampleFor(features) で自動生成される

中央の nav 配列も env リストも手で触らない。1機能 = descriptor 1つ + flag 1つ。