turbo genは、Turborepoに組み込まれたコード生成コマンドです。モノレポに新しいパッケージを足すturbo gen workspaceと、Plopの設定で自作したテンプレートを流すturbo gen runの2系統があります。この記事では、2026年11月11日公開のturbo 2.11.11を空のnpmワークスペースに入れて実行した結果をもとに、設定ファイルの書き方とテンプレート変数、引数を渡して対話なしで動かすときの挙動を整理します。2.11.11では失敗時の終了コードの扱いが変わったため、2.11.7〜2.11.10と同じ手順で比べた結果も載せます。公式ドキュメントに書かれていない失敗パターン(存在しない変数、名前の衝突、--config指定時のパス)も実行ログ付きで扱います。
まとめ:turbo genの要点と導入前に決めること
turbo genはturbo generateの別名で、サブコマンドを省くとrunになる(公式リファレンス)。- パッケージの追加は
turbo gen workspace、ファイル単位の雛形はturbo/generators/config.tsに書くカスタムジェネレーターが担う。 - Plopの本体はインストール不要。TypeScriptで書くときだけ型のために
@turbo/genをdevDependencyに入れる。 - テンプレートには
turbo.paths.rootなどの変数が注入される。未定義変数をパス先頭に置き、その直後にスラッシュを書くと、変数部分が空になってOSのルート直下を指す。権限によっては失敗せず、意図しない場所へ書き込む。 - validateの不通過やaddアクションの失敗は、2.11.7〜2.11.10では終了コード0、2.11.11では1になる(同じ手順で実測)。ただし2.11.11でも、ジェネレーター名が見つからない場合とworkspaceの確認プロンプトに答えなかった場合は0で終わる。CIでは終了コードに加えて生成物の有無で判定する。
パッケージの追加が年に数回で、雛形のファイルが2〜3個しかない規模なら、無理に導入しなくてかまいません。判断基準は後半の使い分けの章にまとめています。
turbo genでできること:turbo generateの別名とworkspace・runの2系統
コード生成はTurborepo 1.10で追加されました。2023年6月1日の公式ブログ「Turborepo 1.10」は「Use turbo gen to generate source code, including creating new workspaces and copying existing ones」と紹介しています。現行版は2026年11月11日公開の2.11.11で、型定義パッケージ@turbo/genも同じ日に同じ版番号で公開されています。Turborepo自体の設定やキャッシュはTurborepoの解説記事で扱っているので、ここではコード生成に絞ります。
| コマンド | 用途 | 主なフラグ |
|---|---|---|
turbo gen workspace |
アプリ/パッケージの追加 | --name --empty --copy --type --destination |
turbo gen run [名前] |
カスタムジェネレーターの実行 | --args --config --root |
turbo gen [名前] |
runの省略形 |
同上 |
@turbo/gen 2.11.11のpackage.jsonを見ると、実行時の依存は@inquirer/promptsとesbuildの2つだけで、node-plop 0.32.3は開発依存として配布物に組み込まれています。リポジトリ側にplopを入れる必要がないのはこのためです。TypeScriptの設定ファイルをそのまま読めるのもesbuildで変換しているからで、ts-nodeなどの追加は要りません。
turbo gen workspaceで新しいパッケージを追加する手順
モノレポにパッケージを足す作業は、package.jsonの名前・scripts・依存関係を既存パッケージからコピーして直す手作業になりがちです。turbo gen workspaceはこの部分を対話式で埋めます。モノレポそのものの構成や採用判断はモノレポの解説記事を参照してください。
空パッケージの生成(--empty)
# 空のアプリを apps/docs に作る
npx turbo gen workspace --name docs --empty --type app --destination apps/docs
# 既存の @repo/ui を packages/ui2 へ複製する
npx turbo gen workspace --name @repo/ui2 --copy @repo/ui --type package --destination packages/ui2
1つ目のコマンドでapps/docsに作られたのは、package.jsonとREADME.mdの2ファイルだけでした。package.jsonのscriptsにはdev・build・test・lintの4つがecho 'Add build script here'のような仮の値で入ります。tsconfigやESLintの設定は作られないので、それらも揃えたいなら次の--copyを使います。
既存パッケージ・GitHub上の例の複製(--copy)
--copyにはモノレポ内のパッケージ名か、ブランチやサブディレクトリを含むGitHubのURLを渡せます。ローカル複製では、元パッケージのsrcだけでなくturbo/generatorsディレクトリまでそのままコピーされ、package.jsonのnameだけが新しい名前に書き換わりました。複製元にジェネレーターがあると、同じ名前のジェネレーターが2つできる点に注意が要ります(後述の失敗パターン)。
リモートから複製する場合について、公式のコード生成ガイドは「Turborepo is unable to verify that your repo has all of the required dependencies, and is using the correct package manager」と注意しています。複製後に依存のバージョンとパッケージマネージャーの差異を手で直す前提で使うのが現実的です。
依存関係の確認プロンプトと非対話実行の限界
名前・種類・作成先をすべてフラグで渡しても、最後に「Add workspace dependencies to “docs”? (Y/n)」と聞かれます。このプロンプトを飛ばすフラグはなく、標準入力を閉じて実行すると何も作らずに終わりました。2.11.11で試しても挙動は同じで、終了コードは0でした。turbo gen workspaceはCIやスクリプトから呼ぶ用途には向かず、人が手元で叩くコマンドと割り切るのが安全です。生成後はpnpm installなどでワークスペースのリンクを張り直します(pnpmのモノレポ運用も参照)。
カスタムジェネレーターの作り方:turbo/generators/config.tsの構成
パッケージ単位ではなく、UIコンポーネントやAPIルートのようなファイル単位の雛形を作るのがカスタムジェネレーターです。PlopのsetGeneratorで、質問(prompts)と実行内容(actions)を定義します。
設定ファイルの置き場所と検出のされ方
設定ファイルはturbo/generators/config.ts(またはconfig.js)で、リポジトリのルートにも各パッケージの中にも置けます。Turborepoはこれらを自動で探し、パッケージごとにまとめて一覧に出します。2.11.11で名前を指定せずにturbo genを実行すると、ルートのジェネレーターはroot/react-component、apps/webのものはweb/academy-styleのようにパッケージ名を前に付けた表示になりました。
公式ガイドは、pnpm・yarn・npm・bunに加えてnub・aubeのワークスペースでの配置例を載せています(この記事の実測はnpmのみ)。パッケージ内のジェネレーターはそのパッケージのルートを起点に動くため、../../のような相対パスを書かずに済みます。2.11.7では、ESM構文(export default)とCommonJS構文(module.exports)のどちらで書いたconfig.jsも、package.jsonに"type"を指定していないリポジトリで読み込めました。
2.7.6時点のガイドには「ESM dependencies are not currently supported within custom generators」という既知の問題が載っていましたが、現行のガイドからはこの注記が消えています。2.11.7とNode.js v26.5.0の組み合わせでは、ESM専用パッケージのnanoid 5.1.16を設定ファイルからimportして実行できました。古い版のTurborepoを使っている場合は、その版のドキュメントで制限を確認してください。
UIコンポーネントを追加するジェネレーターの実装例
この例は、リポジトリ直下から実行し、既存のpackages/ui/package.jsonを更新する前提です。先にpackages/uiを用意し、型定義を入れます。Plop本体は不要です。
npm install --save-dev --save-exact [email protected] @turbo/[email protected]
次の設定は、PascalCaseの名前を受け取ってpackages/ui/srcにコンポーネントを作り、package.jsonのexportsに1行足します。2.11.11で実際に動かした設定そのままです。
import type { PlopTypes } from "@turbo/gen";
import fs from "node:fs";
import path from "node:path";
// package.json の exports を JSON として読み書きする(文字列の追記だとカンマが崩れる)
const addExport: PlopTypes.CustomActionFunction = (answers, _config, plop) => {
const pkgPath = path.join(answers.turbo.paths.root, "packages/ui/package.json");
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
const current = pkg.exports ?? {};
// 文字列や条件付き("import" 等)の exports にサブパスを混ぜると Node.js が読めなくなる
if (typeof current !== "object" || Array.isArray(current) || Object.keys(current).some((k) => !k.startsWith("."))) {
throw new Error("exports がサブパス形式ではないため中止しました");
}
const name = plop.renderString("{{ kebabCase name }}", answers);
const exportKey = `./${name}`;
const exportTarget = `./src/${name}.tsx`;
if (exportKey in current && current[exportKey] !== exportTarget) {
throw new Error(`既存の exports ${exportKey} と競合しています`);
}
pkg.exports = { ...current, [exportKey]: exportTarget };
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
return `exports に ./${name} を追加`;
};
export default function generator(plop: PlopTypes.NodePlopAPI): void {
plop.setGenerator("react-component", {
description: "Add a React component to @repo/ui",
prompts: [
{
type: "input",
name: "name",
message: "Component name (PascalCase)",
validate: (v: string) => /^[A-Z][A-Za-z0-9]*$/.test(v) || "PascalCase only",
},
],
actions: [
{
type: "add",
path: "{{ turbo.paths.root }}/packages/ui/src/{{ kebabCase name }}.tsx",
templateFile: "templates/component.hbs",
skipIfExists: true,
},
addExport,
],
});
}
テンプレートはturbo/generators/templates/component.hbsに置きます。templateFileの相対パスは設定ファイルの場所が起点です。
export function {{ pascalCase name }}() {
return <div data-component="{{ kebabCase name }}">{{ name }}</div>;
}
$ npx turbo gen react-component --args DateTime
>>> Modify "tg2" using custom generators
>>> Actions completed:
• /packages/ui/src/date-time.tsx (add)
• exports に ./date-time を追加 (function)
>>> Success!
skipIfExists: trueを付けたので、同じ名前で再実行すると[SKIPPED] ... (exists)と表示され、後続のaddExportだけが動きます。exportsの追記を文字列のappendでなくJSONの読み書きにしている理由は、失敗パターンの章で説明します。
exportsが文字列("exports": "./index.js")や条件付き("import"などのキー)の書き方になっているパッケージに./date-timeのようなサブパスを足すと、Node.jsがpackage.jsonを読めなくなります。そのためaddExportは、キーがすべて.で始まるオブジェクト以外では例外を投げて止めます。条件付きexportsで試すと「exports がサブパス形式ではないため中止しました」と表示され、package.jsonは書き換わりませんでした。ただし先に動くaddアクションのコンポーネントファイルは残るので、手で消してから既存のexportsを"."配下へ書き直します。
パッケージの雛形一式を生成するaddManyの実装例
turbo gen workspace --emptyが作るのはpackage.jsonとREADME.mdだけです。tsconfigやsrc/index.tsまで社内の型どおりに揃えたいなら、テンプレートのディレクトリを丸ごと展開するaddManyアクションでジェネレーターを作ります。Vercel Academyの教材やZennの解説記事も、パッケージの生成はこの形で書いています。
plop.setGenerator("pkg", {
description: "Add a package from templates/pkg",
prompts: [{ type: "input", name: "name", message: "Package name" }],
actions: [
{
type: "addMany",
destination: "{{ turbo.paths.root }}/packages/{{ kebabCase name }}",
base: "templates/pkg",
templateFiles: "templates/pkg/**",
},
],
});
templates/pkgにpackage.json.hbsとsrc/index.ts.hbsを置いてnpx turbo gen pkg --args "Date Utils"を実行すると、2.11.11では「2 files added」と表示され、packages/date-utilsの下にpackage.jsonとsrc/index.tsが作られました。baseを外して試すと、packages/no-base/pkg/package.jsonのようにpkgの階層が1段残りました。ファイル名の.hbsは、stripExtensionsを書かなくても外れます(既定値が["hbs"]のため)。テンプレートのpackage.jsonに"name": "@repo/{{ kebabCase name }}"と書いておけば、パッケージ名の付け方も揃います。生成後にinstallが要るのはturbo gen workspaceと同じです。
テンプレートで使えるturbo変数とPlopのヘルパー
Turborepoはアクションの実行時に、回答データへturboオブジェクトを注入します。上の設定にanswersをファイルへ書き出すアクションを足して、2.11.11のルートのジェネレーターで中身を確認した結果が次です(configsは整形のため1行にまとめています)。
{
"name": "Card",
"turbo": {
"paths": {
"cwd": "/private/tmp/tg2",
"root": "/private/tmp/tg2",
"workspace": "/private/tmp/tg2"
},
"configs": [
{
"config": { "$schema": "https://turborepo.dev/schema.json", "tasks": { "build": {} } },
"turboConfigPath": "/private/tmp/tg2/turbo.json",
"workspacePath": "/private/tmp/tg2",
"isRootConfig": true
}
]
}
}
公式の@turbo/genリファレンスは、turbo.paths.workspaceをルートのジェネレーターではundefinedと説明しています。2.11.7でも2.11.11でも、実測ではリポジトリのルートと同じパスが入っていました。ルートとパッケージの両方で使い回すテンプレートでworkspaceの有無を条件分岐に使うと、ドキュメントどおりには分岐しません。パッケージ内のジェネレーターではpackages/uiの絶対パスが入り、{{ turbo.paths.workspace }}/src/...と書いたファイルはpackages/ui/srcに作られました。
kebabCase・pascalCase・camelCaseなどの大文字小文字変換はPlopの組み込みヘルパーなので、登録しなくてもテンプレートで使えます。カスタムアクションの中で同じ変換をしたいときは、第3引数のPlop APIのrenderStringを呼ぶと、ヘルパーを自前で書き直さずに済みます。組み込みに無い変換は、設定ファイルの中でplop.setHelper("名前", 関数)を呼んで登録すれば、テンプレートから{{ 名前 値 }}の形で使えます。
引数指定で非対話実行するときの挙動とCIでの確認方法
--argsに回答を並べると、プロンプトを順に埋めて対話なしで実行できます。CIや社内スクリプトでは回答を引数で渡せますが、失敗が終了コードに出るかどうかは版によって違います。同じリポジトリ・同じ手順で、標準入力を/dev/nullにつないで試した結果です。
| 状況 | 表示 | 2.11.7〜2.11.10 | 2.11.11 |
|---|---|---|---|
| 正常終了 | Success! | 0 | 0 |
| validateに通らない値 | PascalCase only | 0 | 1 |
| 既存ファイルへのadd | Failed to run … | 0 | 1 |
| 未定義変数でmkdir失敗 | ENOENT … | 0 | 1 |
| 存在しない名前 | Generator … not found | 0 | 0 |
| workspaceの確認に未応答 | Add workspace dependencies… | 0 | 0 |
今回検証した旧版では、表に挙げた失敗でも終了コードが0でした。turbo本体が@turbo/genの子プロセスの終了コードを捨てて0を返していたためで、2.11.11の修正(vercel/turborepo PR #14985「Propagate child exit code from turbo gen」)で子の終了コードがそのまま返るようになりました。validateの不通過は2.11.8・2.11.9・2.11.10でも0で、1に変わったのは2.11.11からです。
2.11.11でも0のまま残るケースが2つあります。ジェネレーター名が見つからないと@turbo/genは選択プロンプトに切り替わり、標準入力が閉じていればそこで正常終了扱いになります。turbo gen workspaceの確認プロンプトに答えられなかった場合も、何も作らずに0で終わります。名前の打ち間違いやジェネレーターの消し忘れはCIで起きやすいので、終了コードだけでは判定しきれません。turboの版を固定したうえで、生成物の存在とexportsのキーも確かめます。同名のファイルが前回の実行で残っていると、生成に失敗しても存在チェックが通ってしまうため、実行前に存在しないことも確かめておきます。
set -e
FILE=date-time
# 新規生成専用のチェック。既存ファイルがあると成否を判定できないので先に止める
test ! -e "packages/ui/src/$FILE.tsx" || { echo "already exists: $FILE"; exit 1; }
# 2.11.11以降は失敗で非0になる。名前の不在は0のままなので下の確認も残す
npx turbo gen react-component --args DateTime < /dev/null
test -f "packages/ui/src/$FILE.tsx" || { echo "generator failed"; exit 1; }
node -e "
const e = JSON.parse(require('fs').readFileSync('packages/ui/package.json', 'utf8')).exports;
if (e['./$FILE'] !== './src/$FILE.tsx') { console.error('exports not updated'); process.exit(1); }
"
標準入力を/dev/nullにつないでおくと、選択プロンプトで止まったまま待ち続ける事態を避けられます。端末から実行した場合は、名前が見つからないと選択プロンプトが出て入力待ちになります。
turbo genで起きやすい失敗4つと回避策
ここで挙げる4つは、turbo 2.11.7で見つけ、2.11.11(Node.js v26.5.0・npmワークスペース)で再び実行して同じ結果になった挙動です。違いは、2.11.11では失敗時の終了コードが1になる点だけでした。
未定義変数によるOSルート直下への誤出力
Vercel Academyの教材「Turborepo Generators」は、addアクションのパスを{{turboRoot}}/packages/ui/src/...と書いています。2.11.11の回答データにもturboRootはなく、Handlebarsは未定義の変数を空文字にするので、パスは/packages/ui/src/...になります。macOSでは次のエラーで止まりました。
>>> Error - ENOENT: no such file or directory, mkdir '/packages'. Unable to add to ""
>>> Failed to run "root/academy-style" generator
root権限で動くコンテナの中では、エラーにならずに/packagesへ書き込まれるおそれがあります。パスの先頭は{{ turbo.paths.root }}か{{ turbo.paths.workspace }}にし、新しい変数を使う前に、answersを書き出すアクションで実際に入っている値を確認してください。
文字列追記によるpackage.jsonの構文破損
exportsへの追記をappendアクションで次のように書くと、追加した行の末尾にカンマが残ります。
{
type: "append",
path: "{{ turbo.paths.root }}/packages/ui/package.json",
pattern: /"exports": {(?<insertion>)/g,
template: ' "./{{ kebabCase name }}": "./src/{{ kebabCase name }}.tsx",',
}
exportsが空の状態から1件追加すると"./button": "./src/button.tsx",で閉じ括弧に続き、JSONとして不正になります。その後のnpx turbo genはnpmがpackage.jsonを読めず、EJSONPARSEでジェネレーター以前に止まりました。JSONファイルは文字列として追記せず、前述の実装例のようにJSON.parseとJSON.stringifyで書き換えるのが確実です。index.tsへのexport *追記のように、行を足すだけで文法が崩れないファイルならappendで問題ありません。
同名ジェネレーターの衝突とスコープ付きパッケージの修飾名
--copyで@repo/uiを複製すると、turbo/generatorsも一緒にコピーされ、同じ名前のジェネレーターが2つになります。この状態で名前を指定すると、次のように修飾名で指定するよう促されたうえで選択プロンプトに戻ります。
$ npx turbo gen ui-hook --args Toggle
>>> Multiple generators named "ui-hook" found. Use a qualified name to disambiguate:
• @repo/ui/ui-hook
• @repo/ui2/ui-hook
>>> Generator "ui-hook" not found
? Select generator to run
ところが、表示どおり@repo/ui2/ui-hookと打っても「not found」になります。2.11.7でも2.11.11でも結果は同じでした。2.11.11の@turbo/genのソースでも、修飾名は最初の/の位置(indexOf("/"))で分割されており、スコープ付きの名前ではパッケージ名が@repoと解釈されます。root/react-componentや、パッケージ名にスコープが無い場合("name": "ui2")のui2/ui-hookは指定できました。@repo/のようなスコープ付きの命名を使っているリポジトリでは、ジェネレーター名をリポジトリ全体で一意にすると、名前を指定した実行で衝突を避けられます。対話実行では選択リストから対象を選ぶ方法もあります。
非対話で片方を指定したい場合は、--config packages/ui2/turbo/generators/config.tsのように設定ファイルを直接渡すと、そのファイルのジェネレーターだけが対象になりました。ただし--config経由で動かすと、addアクションの相対パスは設定ファイルのパスを起点に解決されます。path: "src/use-{{ kebabCase name }}.ts"と書いたジェネレーターは、2.11.11でENOTDIR: not a directory, mkdir '.../config.ts/src'となって失敗し、パスの先頭を{{ turbo.paths.workspace }}に変えるとpackages/ui2/srcに作られました。複製後は、新しいパッケージからturbo/generatorsを消すか名前を変えます。
既存ファイルへのadd失敗と後続アクションの中断
addアクションは、作成先にファイルがあるとエラーになります。既定のabortOnFail: trueでは、最初のaddが失敗すると後続アクションは「Aborted due to previous action failure」で中断されます(2.11.7・2.11.11とも同じ)。addアクションにabortOnFail: falseを付けると、2.11.11ではaddが失敗しても後続のカスタムアクションが実行され、終了コードは1になりました。再実行がありうるジェネレーターではskipIfExists: trueを付けるか、上書きしてよいファイルに限ってforce: trueを付けます。どちらにするかはファイルごとに決め、手で編集されうるファイルにforceは付けないのが原則です。
Plop単体・Hygen・create-turboとの使い分けと導入を見送る条件
| 手段 | 向く場面 | 最新版(npm) |
|---|---|---|
| turbo gen | Turborepo導入済みのモノレポ | 2.11.11(2026-11-11) |
| Plop単体 | Turborepoを使わないリポジトリ | 4.0.5(2026-01-22) |
| Hygen | 既存のHygenテンプレート資産 | 6.2.11(2022-09-07) |
| create-turbo | リポジトリの新規作成 | 2.11.11(2026-11-11) |
Turborepoを導入済みのリポジトリなら、Plopを単体で入れる理由はほぼありません。turbo genはPlopの設定をそのまま読み、パッケージごとの設定ファイルを自動で集めてくれるからです。Plop単体では、各パッケージの設定をルートの設定からloadする配線を自分で書くことになります。Plop単体を選ぶのは、Turborepoを使わないリポジトリか、ジェネレーターをモノレポの外でも同じCLIとして配りたい場合です。Hygenは最新版が2022年9月から更新されていないため、新規に選ぶ理由は乏しいと判断します。create-turboはリポジトリを最初に作るコマンドで、作ったあとのパッケージ追加はturbo genの役割です。
次のどれかに当てはまるなら、導入は見送ってかまいません。
- パッケージの追加が年に数回で、雛形のファイルが2〜3個しかない。
- 雛形の正解がまだ固まっておらず、テンプレートを毎回直すことになる。
- CIからの自動生成が主目的で、turboを2.11.11以降に上げられず、生成物を確かめる仕組みも別に用意できない。
モノレポのツール自体を比べたい場合は、Nx monorepoの解説が判断材料になります。Turborepo構成を最初から組んだ雛形が欲しいなら、Better-T-Stackのようなスターターも選択肢です。
よくある質問
turbo genとturbo generateは何が違いますか?
違いはありません。公式のCLIリファレンスはturbo genをturbo generateの別名と説明しており、runが既定のサブコマンドなのでturbo genはturbo generate runと同じ動作です。
plopをインストールする必要はありますか?
不要です。@turbo/genがPlopの処理を内部に持っています。TypeScriptで設定を書く場合だけ、型のために@turbo/genをdevDependencyとして入れます。
登録されているジェネレーターの一覧はどう確認しますか?
名前を付けずにturbo genを実行すると、ルートとパッケージごとに分かれた選択リストが出ます。ジェネレーターが1つも無い場合は、雛形の設定ファイルを作るかどうかを聞かれます。
config.tsとconfig.jsのどちらを使えばよいですか?
公式リファレンスは--configの既定値をturbo/generators/config.jsとしていますが、ガイドではconfig.tsも自動で検出されると説明しており、2.11.7ではどちらも読み込めました。プロンプトの回答やPlop APIに型が付くので、TypeScriptのリポジトリならconfig.tsが扱いやすいです。
turbo gen workspaceで作ったパッケージはすぐ使えますか?
ファイルが作られるだけなので、他のパッケージから参照するにはパッケージマネージャーのinstallを実行してリンクを張り直します。--emptyで作った場合はscriptsが仮のechoなので、ビルド手順も書き換えが必要です。