💧

Non-decoupled Drupal プロジェクトでの TypeScript によるフロントエンド開発環境の試作

2021/12/31に公開

Drupal Advent Calendar 2021 の24日目の記事です。

はじめに

Non-decoupled Drupal プロジェクトのフロントエンド開発環境は、TypeScript の型定義が公式で提供されていないためか、TypeScript を使ったフロントエンド開発の記事があまり見かけられないように見えます。そこで、試しに拙著の Drupal Advent Calendar 2021 の 13 日目の記事で触れた、Drupal コアの TypeScript の型定義を基に、フロントエンド開発環境を試作してみました。

TL;DR

デモのレポジトリから git clone できます。

試作したフロントエンド開発環境の構成について

フロントエンド開発環境の構成は下記のようになります。

  • Yarn v1 + monorepo
  • TypeScript
  • ESLint

Yarn v1 + monorepo

Yarn v1 の採用理由は、Drupal コアのフロントエンド開発環境に合わせたためです。
monorepo の採用理由は、Drupal のテーマやモジュール(、プロファイル)の設置ディレクトリ構成が monorepo にあわせやすそうな形になっていると考えたためです。
Drupal プロジェクトのモジュールのディレクトリ構造は、Composer を使用して Drupal をインストールした場合、composer/installers が依存関係に入ることが多いため、大抵下記のようになります。contrib ディレクトリにプロジェクトに必要な貢献モジュールがインストールされ、custom ディレクトリにプロジェクト独自の自作モジュールがそれぞれ設置されます。(下記ならば、module_1、module_2、module_3)
自作モジュールごとに必要となる依存関係や型定義は異なるため、各モジュール毎に定義する方が理にかなっていると思い、monorepo を採用しました。

DRUPAL_ROOT/
    ├ modules
    │    ├contrib/
    │    └custom/
    │         ├ module_1/
    │         ├ module_2/
    │         └ module_3/

ちなみに、このディレクトリ構造は、テーマも同様になっています。

TypeScript

個人的に、Non-decoupled Drupal プロジェクトであっても TypeScript を使用すべきだという立場です。
たしかに、Non-decoupled Drupal プロジェクトの場合、フロントエンド開発は小さなスクリプトで済むことは多いので、TypeScript を使用しないという選択肢はあるかもしれません。
しかし、開発する内容によっては定番の JavaScript ライブラリの機能を使用したカスタム画面を用意しないといけない場合もあると思います。
そのようなとき、TypeScript を使用しない旧来の開発ならば、開発者は API のドキュメントとソースコードを見たり、場合によってはブラウザ上で動作を実際にさせたときの変数や引数、戻り値を把握してから開発をしていたと思います。一方、TypeScript を使用した上でライブラリが TypeScript の型定義を用意していれば、開発者はその型定義と API ドキュメントを信用すればすぐに開発を進められるようになる場合が多くなると思います。機能開発において使用しなければならないライブラリの勉強に使う時間を短縮出来るのならば、TypeScript を使わない手はないと思います。

ESLint

ESLint の設定は Drupal コアの設定を TypeScript 向けに拡張して使います。具体的には、@typescript-eslint/eslint-plugin@typescript-eslint/parser を追加して、.eslintrc.cjs をルートディレクトリに作成します。

レポジトリのコードの解説

ルートディレクトリ

tsconfig.json

TypeScript と Yarn の組み合わせで monorepo を設定するブログ記事の内容を参考に、tsconfig.json に references の設定を行います。

package.json

monorepo なので、package.json の記述に workspaces の記述と private: true を追加します。下記のように package.json を置いているディレクトリからのカスタムモジュールとテーマのパスを設定します。

package.json
  "workspaces": [
    "./path_to/modules/custom/*",
    "./path_to/themes/custom/*"
  ],

.eslintrc.cjs

Drupal コアの設定を読み込んで拡張するので、CommonJS 形式一択です。require()
Drupal コアの設定を読み込み、これをベースに TypeScript を使用したプロジェクトのルールを設定します。
設定の際は、TypeScript ESLint の Linting your TypeScript Codebaseを参考に、parserplugins を設定します。extends は、Drupal コアの YAML の設定は不要だと思うので、"airbnb""plugin:prettier/recommended" のみを使用し、TypeScript 関係の二つを追加します。(実際には、冗長な記述だそうです)

.eslintrc.cjs
projectSettings.extends = [
  "airbnb",
  "plugin:prettier/recommended",
  "plugin:@typescript-eslint/eslint-recommended",
  "plugin:@typescript-eslint/recommended",
];

Lint のチェックでは no-unused-vars@typescript-eslint/no-unused-vars のように ESLint と TypeScript ESLint で重複している設定もあるので、気になる場合は片方を無効にしておいても良いと思います。
型定義ファイルに関しては、Lint する意義があまりないので、*.d.ts はチェック対象から外しています。

tsconfig.lint.json

各モジュール・テーマのディレクトリで、tsc コマンドを使って型チェックをする際のベースとなる設定ファイルを設置します。ここの型チェックは strict: true で問題ないと思います。各モジュール・テーマディレクトリで、Drupal コアの TypeScript の型定義を使用する場合、/node_modules/@types 以下にディレクトリができないので、typeRoots を下記のように設定する必要があります。

tsconfig.lint.json
    "typeRoots": [
      "node_modules/@types",
      "node_modules/@tom-konda/typedef-drupal-core",
    ],

各モジュール・テーマディレクトリ

各モジュール・テーマディレクトリには、TypeScript ファイルを置くディレクトリ ts/ を作成し、package.json と tsconfig.json を設置します。

tsconfig.json

先述の tsconfig.lint.json は型チェックの際に必要なので、extends を読み込んで拡張していきます。
TypeScript と Yarn の組み合わせで monorepo を設定するブログ記事の内容を参考に、各 composite: true の設定しています。
使用する型定義は、各モジュール・テーマで変わるのでそれに合わせて types を設定します。
最後に include に先述した ts/ ディレクトリの内容が対象になるように設定をします。
以上をまとめると下記のようになります。

tsconfig.json
{
  "extends": "../../../../tsconfig.lint.json",
  "compilerOptions": {
    "composite": true,
    // 下記はモジュール・テーマ毎に可変
    "types": ["jquery", "jquery.once", "jquery.cookie","drupal", "drupal.ajax"],
  },
  "include": [
    "./ts/**/*.ts",
  ]
}

package.json

各モジュール・テーマディレクトリで型のチェックを行うため、scripts に下記のような記述を追加します。(TypeScript の変換は不要なので、実行時に --noEmit を付けています)

modules/custom/module_1/package.json
  "scripts": {
    "type-check": "tsc -p ./ --noEmit"
  },

frontend/ ディレクトリ

TypeScript から JavaScript へのトランスパイルは Babel で行うため、Node.js でその処理を記述します。Babel のプリセット @babel/preset-typescript を利用してトランスパイルします。
当然変換対象から、*.d.ts は除外します。

おわりに

この記事では、試作した Non-decoupled Drupal プロジェクトでの TypeScript によるフロントエンド開発環境の構成とコードの解説をしました。
あくまでこれは試作であるため、これをベースに来年から Non-decoupled Drupal プロジェクトでの TypeScript 開発が盛り上がってくれればと願います。

Discussion