レシピ · 9手 · 1プロンプト実装

新しいモジュールをプラットフォームに追加する(メタレシピ)

新機能を「選べる(event.config / create-event-app)× dev で動く(starter 参照配線)× 1プロンプト(recipe)× published 章(manual)」の4面が揃った状態でプラットフォームに登録する

解説 ch.48

このレシピはプラットフォーム自己増殖プロトコルである。他のレシピが「実装済みモジュールをイベント app に配線する」手順なのに対し、これは「モジュール自体を新しく作ってプラットフォームに登録する」手順 — つまり add-<name> レシピを1本増やすためのレシピ。

直近の実例は @event/motion(recipe add-motion / 第47章)で、まさにこのプロトコルで追加された。以下の各ステップで motion を照合先として参照できる。

完了条件(4面テスト):

  1. 選べるevent.config.ts の flag / create-event-app--features に現れる(基盤層なら flag 不要と明記されている)
  2. dev で動くapps/starter に参照配線が入り yarn workspace starter dev で動作する
  3. 1プロンプトrecipes/add-<name>.ts が自己完結ランブックとして存在し yarn lint:recipes が import 照合を通す
  4. published 章 — マニュアル章が published/guide/<slug> に現れる

登録点は全部で 18 箇所。漏れると「動くが選べない」「選べるがマニュアルに無い」の非対称が生まれる:

#登録点何を書くか
1packages/<name>/package.jsonパッケージ宣言(exports は TS ソース直)
2packages/<name>/tsconfig.jsonreact-library / base の extends
3packages/<name>/src/index.ts公開面(全 export の集約点)
4apps/starter/package.jsondependencies に "@event/<name>": "*"
5apps/starter/next.config.tstranspilePackages に追加
6apps/starter/src/event.config.tsfeatures flag(基盤層なら不要)
7apps/starter/src/event.config.tsFEATURES descriptor(基盤層なら不要)
8apps/starter/src/app/…参照 UI(page / route。features.isEnabled gate)
9packages/create-event-app/bin.mjsKNOWN_FEATURES 配列(基盤層なら不要)
10packages/create-event-app/bin.mjsenv 生成 if ブロック(env が要る機能のみ)
11apps/manual/src/content/recipes/add-<name>.ts自己完結レシピ本体
12apps/manual/src/content/recipes/index.tsimport + RECIPES(依存順の位置)
13apps/manual/src/content/chapters/<slug>.tsマニュアル章本体
14apps/manual/src/content/index.tsimport + CHAPTERS
15apps/manual/src/lib/toc.tsc(n, "…", "published")
16apps/manual/src/content/modules-catalog.tsMODULES エントリ(/modules と統計の真実源)
17apps/manual/src/content/packages-catalog.tsPACKAGES エントリ(/reference の真実源)
18AGENTS.mdレシピ表 + パッケージ表に各1行

登録点 11〜17 はすべてデータ(TS の配列)なので、登録した瞬間に機械可読レイヤ (/llms.txt / /llms-full.txt / /api/registry — 第48章)にも自動で載る。手書きの同期は AGENTS.md だけ。

1. packages/<name> を scaffold する

命名は @event/<name>(ディレクトリは packages/<name>)。React component を含むかで 2 系統:

React あり(component を export する — motion / cms/ui 型)— packages/<name>/package.json:

{
  "name": "@event/<name>",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "exports": {
    ".": "./src/index.ts"
  },
  "scripts": {
    "typecheck": "tsc --noEmit"
  },
  "peerDependencies": {
    "react": ">=19"
  },
  "devDependencies": {
    "@event/tsconfig": "*",
    "@types/react": "^19.2.0",
    "typescript": "^5.7.3"
  }
}

packages/<name>/tsconfig.json:

{
  "extends": "@event/tsconfig/react-library.json",
  "include": ["src"]
}

React なし(純関数層 — design / networking 型): peerDependencies の react と @types/react を外し、tsconfig の extends を "@event/tsconfig/base.json" にする。

要点(internal-packages パターン — root CLAUDE.md 準拠):

— 消費側 app の transpilePackages がコンパイルする(§2)

subpath が要るときは exports"./client" 等のキーを足す(例: @event/cms./ui / ./client

server で描画可能なもの — MotionStyles のような純粋描画 — には付けない)

packages/<name>/src/index.ts は doc コメント + 再 export だけにする(motion の実物が手本):

/**
 * @event/<name> — 一言でこのパッケージの責務。
 *
 * 設計判断・配線方法(recipe add-<name> 参照)をここに数行で書く。
 */

export { something } from "./something";
export { SomeComponent, type SomeComponentProps } from "./SomeComponent";

作成後、workspace として認識されることを確認:

yarn install
yarn workspace @event/<name> typecheck

2. starter に依存を配線する

apps/starter/package.json の dependencies に追加:

"@event/<name>": "*"

apps/starter/next.config.tstranspilePackages 配列に "@event/<name>" を足す (TS ソース直 exports なのでこれが無いとビルドできない)。root で yarn install

3. event.config.ts — FeatureDescriptor + flag

先に判断: この機能は flag が要るか。

このステップと §5 の KNOWN_FEATURES をスキップし、レシピに「feature flag 不要」と明記する(add-motion の書き方を踏襲)

feature 型の場合、apps/starter/src/event.config.tsfeatures に flag を足し:

features: {
  // …既存…
  <name>: true, // ← @event/<name> の一言説明
},

FEATURES 配列に descriptor を1つ足す(第42章のフルフロー。nav・env・章番号が自己記述で追従する):

{
  key: "<name>",
  label: "<日本語ラベル>",
  description: "<何が動くか>(@event/<name>、dev の永続先も書く)",
  nav: [
    { id: "<name>-admin", group: "<nav グループ>", label: "<nav ラベル>", href: "/admin/<name>", code: "<4文字コード>", order: 1 },
  ],
  env: [
    { name: "SOME_TOKEN", required: false, description: "何に使うか(未設定時の挙動も書く)" },
  ],
  manualChapters: [<n>], // §7 で採番する章番号
  // requires: ["cms"], // 依存機能があれば(違反は composeFeatures が起動時 fail-fast)
},

4. starter に参照 UI を置く

apps/starter/src/app/ に最小の page / route を置く。エンジンコードをイベント app に書かない (鉄則 — ロジックはパッケージへ、app には配線だけ)。feature 型は冒頭 gate が作法:

// apps/starter/src/app/admin/<name>/page.tsx(feature 型・admin の作法)
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 Page() {
  if (!features.isEnabled("<name>")) notFound();               // ① 無効機能は 404
  const cookie = (await cookies()).get(adminAuth.cookieName)?.value;
  if (!adminAuth.isAuthedFromValue(cookie)) redirect("/login"); // ② 未認証は /login
  // …ここからパッケージの export を呼ぶ配線だけ…
}

基盤層は gate 不要で layout / page に直接配線する。実例(add-motion §2〜3 の転写):

import { MotionStyles, Reveal } from "@event/motion";
// layout の <head> に <MotionStyles /> を1回 → page で <Reveal>…</Reveal>

5. create-event-app に登録する

packages/create-event-app/bin.mjsKNOWN_FEATURES 配列(ファイル冒頭)に "<name>" を足す:

const KNOWN_FEATURES = ["cms", "sessionCard", /* …既存… */, "<name>"];

これで node packages/create-event-app/bin.mjs <event> --features=…,<name> が通り、 生成される event.config.ts の flag に反映される(starter 複製方式なので §2〜4 の配線は自動で入る)。

if (selected.includes("<name>")) {
  envVars.set("SOME_TOKEN", { required: false, description: "何に使うか" });
}
if (selected.includes("<name>") && !selected.includes("cms")) selected.push("cms");

基盤層(flag 無し)はこのステップ全体をスキップ(starter 複製にソースごと入るため)。

6. recipes/add-<name>.ts — 1プロンプト・ランブック

apps/manual/src/content/recipes/add-<name>.tsRecipe 型(recipes/types.ts)で書く。 自己完結が絶対条件: ファイルパス・完全なコード・コマンド・検証まで含め、「〜を適宜書く」で終わらせない。 body のコード block には実装をそのまま転写する(要約・擬似コードにしない)。

import type { Recipe } from "./types";

const recipe: Recipe = {
  id: "add-<name>",
  title: "<機能>を追加する",
  goal: "<何が完成するか1文>",
  prerequisites: ["new-event"],
  features: ["<name>"], // 基盤層なら []
  chapters: [<n>],
  steps: <目安手数>,
  ready: true,
  body: `…自己完結ランブック(手本: add-design / add-networking / add-motion)…`,
};

export default recipe;

apps/manual/src/content/recipes/index.ts に import 1行 + RECIPES 配列へ依存順の位置に挿入する。

recipe-lint が import を照合する: body 内コード block に書いた @event/<name> からの named import 文(import { X } … 形式)は tooling/recipe-lint/lint.mjspackages/<name>/package.jsonexports を辿って実 export 集合と静的照合する (yarn lint:recipes)。存在しない API 名を書くと CI が落ちる — 推測 API はここで構造的に排除される。 逆に言えば、レシピに書く import 名は必ず src/index.ts から export しておくこと。

7. マニュアル章 — chapters/<slug>.ts + 2 つの登録点

章番号 napps/manual/src/lib/toc.ts既存最大 + 1 で採番する。

apps/manual/src/content/chapters/<slug>.tscontent/types.tsChapter 型):

import type { Chapter } from "../types";

/** 第<n>章 — 一言。 */
const chapter: Chapter = {
  n: <n>,
  slug: "<slug>",
  title: "<章タイトル>",
  packages: ["@event/<name>"], // サイト機能の章なら []
  body: `…What/Why/How。設計判断を実 export・実コードで書く(手本: design-customization)…`,
};

export default chapter;

登録点は2つ:

  1. apps/manual/src/content/index.ts — import 1行 + CHAPTERS 配列に追加(n 昇順 sort 済みなので位置は末尾でよい)
  2. apps/manual/src/lib/toc.ts — 該当 Part(拡張系は Part VIII「プラットフォーム拡張」)に

c(<n>, "<toc タイトル>", "published") を足す。"published" にしない限り /guide に現れない

8. カタログ + AGENTS.md を同期する

3ファイルに各1エントリ(登録点 16〜18):

{
  key: "<name>",
  label: "<日本語ラベル>",
  pkg: "@event/<name>",
  status: "ready",
  what: "<何がどう動くか — 設計判断込みで2〜3文>",
  enable: ["<name>: true(基盤層なら recipe add-<name>(flag 不要))", "<必要 env>", "<配線の要点>"],
  chapters: [<n>],
},
{
  name: "@event/<name>",
  entry: ["."],
  summary: "<1行サマリ>",
  exports: [
    { name: "mainExport / SubExport", desc: "<何をするか>" },
  ],
},

パッケージ表に1行(| \@event/<name>\ | <役割> | \.\ |

9. 検証

yarn workspace @event/<name> typecheck   # パッケージ単体
yarn workspace starter typecheck         # 参照配線
yarn workspace manual typecheck          # レシピ・章・カタログ
yarn lint:recipes                        # レシピ import ↔ 実 export の照合
yarn build                               # turbo 全 workspace ビルド
yarn workspace starter dev   # → 参照 UI が動く(feature 型は flag off で 404 になることも確認)
yarn dev:manual              # → /recipes/add-<name> と /guide/<slug> が表示される

完了条件(4面テスト再掲): ① create-event-app --features=<name> で選べる(基盤層は「flag 不要」がレシピに明記) ② starter dev で動く ③ yarn lint:recipes green の自己完結レシピがある ④ /guide/<slug> が published で読める。 この4面が揃って初めて「プラットフォームに追加された」と言える — コードが merge されただけの状態を完了と呼ばない。