Apollo CLI の codegen から GraphQL Code Generator に移行する

Apollo CLIcodegen から GraphQL Code Generator に移行したくなり、移行しました。移行するにあたって、一度に全部変更すると破滅するので、以下の作戦で行いました。

  • GraphQL Code Generator を導入
  • apollo client:codegen と GraphQL Code Generator を共存させる
  • 徐々に GraphQL Code Generator で生成されたコードに置き換えていく
  • 最後に apollo client:codegen 記述を package.json から削除

といった具合で進めます。

GraphQL Code Generator を導入

こちらに導入方法をまとめてあるので良かったらどうぞ。導入だけであれば、特に既存の何かと衝突したりはせずすんなり入ると思います。 funnelbit.hatenablog.com

apollo client:codegen と GraphQL Code Generator を共存させる

多くの場合、package.json などにコード生成タスクを書いていて、そこで apollo client:codegen を動かしているプロダクトが多いと思います。これはそのままにしておいてください。また GraphQL Code Generator を導入したことで GraphQL Code Generator もコード生成処理が package.json に書かれていると思いますが、これもそのままにしてください。そして少し無茶な感じですが apollo client:codegen と GraphQL Code Generator のコード生成を同時に走らせても破滅しないようにします。

とはいえ、デフォルトの状態では駄目な可能性があります。GraphQL Code Generator でコード生成をした後に apollo-codegen のコード生成プロセスを実行してみてください。場合によってはエラーが出ます(もしでなければここは読み飛ばしてください)。

intro-graphql-codegen ● ⍟4  npm run dev:apollo # apollo client:codegen です
 
 > app@0.1.0 dev:apollo /app
 > apollo client:codegen --target=typescript --outputFlat --customScalarsPrefix=GraphQL --watch ./src/types/graphql.ts
 
 CLIError: Error in "Loading queries for Unnamed Project": Error: ️️There are multiple definitions for the `GetUser` operation. Please rename or remove all operations with the duplicated name before continuing.
     at Object.error (/app/node_modules/@oclif/errors/lib/index.js:26:15)
     at Generate.error (/app/node_modules/@oclif/command/lib/command.js:60:23)
     at OclifLoadingHandler.showError (/app/node_modules/apollo/lib/OclifLoadingHandler.js:28:22)
     at OclifLoadingHandler.handle (/app/node_modules/apollo/lib/OclifLoadingHandler.js:13:18) {
   oclif: { exit: 2 },
   code: undefined
 }
   ✖ Loading Apollo Project
     → Error initializing Apollo GraphQL project "Unnamed Project": Error: Error in "Loading queries for Unnamed Project": Error: ️️There are multiple definitions for the `GetUser`

GetUser が複数定義していると言われて怒られています。これは、GetUser の定義が graphql-codegen によって生成されており、そこと競合しているとみなされエラーとなっているためです。

// とあるコンポーネント内。gql が定義されている
 ...
 gql`
   query GetUser($id: ID!) {
     node(id: $id) {
       id
       ... on Entry {
         title
       }
     }
   }
 `;
 ...
//  GraphQL Code Generator によって作られた graphql.ts ファイル内
 ...
 export const GetUserDocument = gql` // ここで定義されているので、二重にあると認識されている
     query GetUser($id: ID!) {
   node(id: $id) {
     id
       ... on Entry {
         title
       }
   }
 }
     `;
     ...

graphql-codegen で作られた GetUserDocument は、各コンポーネントで import して利用すると便利なので、このままにしておきたいです。とはいえこのままでは apollo-codegen でコード生成できないので、何かしらの対策が必要になります。

そのための策としては、単純に apollo-codegen が graphql-codegen によって作られたファイルを見ないようにすると良いです。

// package.json
 ...
 "dev:apollo": "apollo client:codegen --target=typescript --includes='src/**/!(graphql).{ts,tsx}' --outputFlat --customScalarsPrefix=GraphQL --watch ./src/types/graphql.ts",
  ...

ポイントは --includes='src/**/!(graphql).{ts,tsx}' で、ここで graphql-codegen によって作られたコード src/generated/graphql.ts を含まないようにしています。この状態で apollo-codegen のコード生成を走らせるとうまくいきます。

徐々に GraphQL Code Generator で生成されたコードに置き換えていく

import や gql の宣言箇所、型などいくつか書き換える必要があります。以下に例を diff で示します。

  import { gql } from "@apollo/client";
 - import { CreatedWorksContent_createdWorks } from "types/graphql"; // 1
   import {
 -   createdWorkFragment,
 -   ListCreatedWork,
 - } from "./elements/ListItem"; // 2
 +   CreatedWorkContentFragment,
 +   CreatedWorkContentFragmentDoc,
 + } from "generated/graphql"; // 3
 + import { ListCreatedWork } from "./elements/ListItem";
  
 - export const createdWorksFragment = gql` // 4
 + gql` 
     fragment CreatedWorksContent on RegisteredViewer {
       createdWorks {
         id
         ...CreatedWorkContent
       }
     }
 -   ${createdWorkFragment}
 +   ${CreatedWorkContentFragmentDoc} // 5
   `;
   
   interface ListCreatedProps {
 -   createdWorks: CreatedWorksContent_createdWorks[] | null;
 +   createdWorks?: CreatedWorkContentFragment[] | null; // 6
   }
   
   export const ListCreated: React.FC<ListCreatedProps> = ({
...
  1. types/graphqlapollo codegen によって生成された定義が入っている。これは利用しないので import を削除する
  2. ここではわかりにくいですが、 ListItem コンポーネントの中でも apollo codegen で生成されたコードを GraphQLCodegen によって生成されたコードにおきかえています。ListItem は Fragment Collocation として props を要求しているので、結果として import が変わっている、ということになります。
  3. generated/graphql は graphql-codegen によって生成された定義が入っています。これからはこちらを利用するので import します。ここには 2 で削除した項目が(変数名は若干違うが中身は同じ)入っていることになります。
  4. graphql-codegen はここに相当する変数を export してくれているので、必要がなくなります。
  5. 4 によって export された変数を代わりにここで使います。
  6. apollo codegen は Fragment 内の各キーごとに型としていました。graphql-codegen は Fragment ごとの型が生成されます。これに則ると、このコンポーネントは自身で定義した Fragment の中の型を要求するのではなく、自信で定義した Fragment そのものの型を要求する形になります。

意外と様々な箇所を変えることになるので、最も下層のコンポーネントから手を入れて、そこから影響のあるコンポーネントも変えていく、というスタイルが良さそうです。