😃

Turborepoのgeneratorでmonorepoパッケージの雛形を自動生成する

に公開

こんにちは、monorepoでpackageを管理しているようなプロジェクトにおいて、
新しくpackageを追加する時に毎回既存のpackageをコピペしてゴニョゴニョする、なんて経験無いですか?
私はよくあります、、笑

turborepoを利用しているプロジェクトではgeneratorを使うことで、
動的にstarter的なpackageを自動生成することが出来るのでそれについて紹介します。

asciicast

turbo genコマンドについて

TUIの対話形式でファイルを生成出来るコマンドです。
内部的にはPlopがファイル生成部分を担っており、
動的な部分はHandlebarsというテンプレートエンジンが採用されています。
また、TUI部分は Inquirer.jsが利用されています。

https://turborepo.com/docs/guides/generating-code

ユースケースについて

雛形を生成することに特化しており、
以下のようなユースケースで活用出来るかと思います。

  • monorepo packageの雛形生成
  • componentの雛形生成
  • storybookのstoryファイルの雛形生成
  • routerやcontroller、model、repositoryの雛形生成

ユースケースによってはコピペして修正した方が早いなんてこともあるので、
運用工数やコピペ後の修正ミスによるリスクを天秤にして導入判断をするのが良いと思います。

exampleの紹介

turborepoは任意のディレクトリ(rootも含む)から turbo/generators/config.ts を探索します。

ユースケースによって各々配置場所を決めればと思いますが、
monorepoのpackageを生成するという責務なためroot直下から配置する構成で自分は組んでいます。

https://github.com/huuyafwww/trastocker/tree/develop/turbo/generators

config.ts

基本的にはplopのドキュメントを参照しながらどんな対話ベースで雛形を生成するか定義することになります。

以下は私が個人開発で利用しているconfigです。

import type { PlopTypes } from '@turbo/gen';

const PACKAGE_TYPES = {
  app: 'apps',
  definition: 'definitions',
  helper: 'helpers',
  library: 'libraries',
};

type PackageType = keyof typeof PACKAGE_TYPES;

export default function generator(plop: PlopTypes.NodePlopAPI): void {
  plop.setHelper('package', (type: PackageType) => PACKAGE_TYPES[type]);
  plop.setHelper('library', (type: PackageType, name: string) => ({
    app: `app-${name}-server`,
    definition: `${name}-definition`,
    helper: `${name}-helper`,
    library: name,
  }[type]));

  plop.setGenerator('package', {
    description: 'Create a new package',
    prompts: [
      {
        type: 'list',
        name: 'type',
        message: 'What type of package do you want to create?',
        choices: ['app', 'definition', 'helper', 'library'],
      },
      {
        type: 'input',
        name: 'name',
        message: 'What is your package name?',
      },
    ],
    actions: [
      {
        type: 'add',
        path: '{{package type}}/{{library type name}}/package.json',
        templateFile: 'templates/package.json.hbs',
      },
      {
        type: 'add',
        path: '{{package type}}/{{library type name}}/eslint.config.ts',
        templateFile: 'templates/eslint.config.ts',
      },
      {
        type: 'add',
        path: '{{package type}}/{{library type name}}/tsconfig.json',
        templateFile: 'templates/tsconfig.json',
      },
    ],
  });
}

default exportした関数の引数にplopが降ってくるのでそれをベースに組むことになります。

plop.setGenerator

第一引数はgenerator名です。
turbo gen 時に指定するgenerator名になります。

turbo gen generator名

第二引数のactionsは対話ベースの入力結果から行うactionを指定します。

以下の場合、 templateファイルとして templates/package.json.hbs が指定されており、
pathに指定の場所へ生成されます。

{
    type: 'add',
    path: '{{package type}}/{{library type name}}/package.json',
    templateFile: 'templates/package.json.hbs',
},

typeは add 以外にも modifyappend などもあるので詳しくは公式を参照ください。

https://plopjs.com/documentation/#built-in-actions

pathの中にはこの後紹介する promptssetHelper を元にした動的な値を指定することができます。

第二引数のpromptsは対話ベースの設定です。

以下の場合、 choices に定義してある list の中から既定値を選択させることができます。
packageの種別を選択させるために利用しており、 name に指定の値はtemplateエンジンの handlebars を通して利用出来ます。

{
    type: 'list',
    name: 'type',
    message: 'What type of package do you want to create?',
    choices: ['app', 'definition', 'helper', 'library'],
},

以下はtypeが input なので自由入力可能なプロンプトです。

{
    type: 'input',
    name: 'name',
    message: 'What is your package name?',
},

他にもcheckboxやy/Nの確認プロンプト、パスワードを入力させるプロンプトも設定可能なので、
詳しくはInquirer.jsのドキュメントを参照してください。

https://github.com/SBoudrias/Inquirer.js/blob/main/packages/inquirer/README.md#prompt-types

plop.setHelper

HandlebarsのregisterHelperが利用されています。

https://handlebarsjs.com/guide/#custom-helpers

第一引数はhelper名で第二引数は関数を定義します。

関数の引数には plop.setGeneratorprompts で設定した name の値を入れて利用することが出来るので、

const PACKAGE_TYPES = {
  app: 'apps',
  definition: 'definitions',
  helper: 'helpers',
  library: 'libraries',
};

type PackageType = keyof typeof PACKAGE_TYPES;

plop.setHelper('package', (type: PackageType) => PACKAGE_TYPES[type]);
plop.setHelper('library', (type: PackageType, name: string) => ({
  app: `app-${name}-server`,
  definition: `${name}-definition`,
  helper: `${name}-helper`,
  library: name,
}[type]));

上記のように定義されていたとして、
actionsのpathを

{{package type}}/{{library type name}}/package.json

のようにすることで、

  • package helperの第一引数にtype(['app', 'definition', 'helper', 'library']の何れか)を代入した結果
  • library helperの第一引数に前述のtypeを第二引数に自由入力のnameを代入した結果

を組み合わせて生成場所を指定することが可能です。

また、 actions で指定した templateFile のファイル内でもこのhelperやnameが利用可能となっております。

私の場合はpackage名を動的に生成するよう組んでます。

{
  "name": "@trastocker/{{library type name}}",
  "version": "0.1.0",
  "private": true,
  "description": "",
  "type": "module",
  "exports": {
    "require": {
      "types": "./dist/index.d.cts",
      "default": "./dist/index.cjs"
    },
    "import": {
      "types": "./dist/index.d.mts",
      "default": "./dist/index.mjs"
    }
  },
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.cts",
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "pkgroll --minify",
    "build:watch": "pkgroll --watch",
    "clean": "rimraf node_modules dist .turbo",
    "lint:code": "eslint . --cache",
    "lint:type": "tsc --pretty --noEmit"
  },
  "dependencies": {},
  "devDependencies": {
    "@huuyafwww/eslint-config-common": "^1.1.3",
    "@huuyafwww/eslint-config-node": "^1.1.3",
    "@huuyafwww/tsconfig-common": "^1.1.1",
    "pkgroll": "^2.12.1",
    "typescript": "^5.8.3"
  }
}

必ず動的にしなければいけないことは無いので、
固定のファイルを生成場所へコピーするだけでも利用することは可能です。

tsconfig.jsonやeslint.config.tsはpackageのユースケースによって設定内容が大きく変わると思うので、
生成後に雛形から自分で修正するというスタイルで利用すれば良いと思います👍

ユースケースによっては全然違う活用方法もあるので、
必要に応じてそれぞれの公式リファレンスを参照しながら組んでもらえればと思います。

以上です。

Discussion