🫥

graphql-request v7をCommonJS環境で使い続ける

2024/08/15に公開

graphql-request@7.0.0にてCommonJSがサポート対象外に

Release Noteに記載されている通り、v7以降ではCommonJSサポートが削除されESMのみサポートとなりました。

7.0.0
BREAKING CHANGES

  • 2a121c6 remove inlined graphql websocket code
  • 99a192e add spec compliant default Accept header (#618)
  • 0e53aed replace GraphQLClientRequestHeaders with built-in HeadersInit type (#616)
  • c3a309f remove support for CommonJS (#607)
  • 6efcc0d remove cross-fetch polyfill
  • Valid URL must be passed (no more path-only strings like /foo) (#745)

このため、Node.jsをCommonJSで実行している場合、requireやTypeScriptのimportでパッケージを利用することができません。例えば、以下のようなコードがエラーとなります。

import { ClientError, GraphQLClient } from 'graphql-request';
// error TS2307: Cannot find module 'graphql-request' or its corresponding type declarations.

対処法

CommonJS環境であっても、dynamic importを使用することでESMをimportすることができます。ただし、非同期のimportにしか対応していないためコードを書き換える必要があります。

例:

async function createClient() {
  const { GraphQLClient } = await import('graphql-request');
  return new GraphQLClient(...);
}

TypeScriptの場合はmodule: "NodeNext"を使用

TypeScriptの場合、module: "CommonJS"を使用しているとawait importがrequireに変換されてしまいます。これを防ぐため、以下のようにtsconfig.jsonを書き換えます。

  {
    "compilerOptions": {
-     "module": "CommonJS",
+     "module": "NodeNext",
      // moduleResolutionもNodeNextに変更するか、削除する
-     "moduleResolution": "Node",
      ...

import typeによる型のみのimportを行う

TS5.3で追加されたimport type ... with構文を使用することで型のみをimportできます。
例えば、GraphQLResponse型をコード中で使用したい場合は以下のようになります。

import type { GraphQLResponse } from 'graphql-request' with {
  "resolution-mode": "import" // ESMとしてimport文を処理する
};

参考: https://github.com/microsoft/TypeScript/issues/49721

@graphql-codegen/typescript-graphql-requestでの対応

生成結果をESMにする

@graphql-codegen/typescript-graphql-requestでは生成結果に普通にgraphql-requestのimport文が埋め込まれてしまうため、上述のようなdynamic importやimport typeを用いる方法が使用できません。このため、代わりに生成されるコード自体をESMファイルとし、それを使用側でdynamic importすることにします。

codegen.ymlにemitLegacyCommonJSImport: falseを追加することでESMとして動作するコードを出力できます。また、出力先の拡張子は.mtsとします。

  overwrite: true
  schema:
    - ...
  documents: ['./src/**/*.graphql']
+ emitLegacyCommonJSImports: false
  generates:
-   src/hoge/generated.ts:
+   src/hoge/generated.mts:
      plugins:
        ...

使用する際には以下のようにimportします。ESMをimportする際には拡張子.mjsを付ける必要がある点に注意してください。

async function createClient() {
  const { GraphQLClient } = await import('graphql-request');
  const { getSdk } = await import('./generated.mjs');
  return getSdk(
    new GraphQLClient(...)
  );
}

gqlの代わりにDocumentNodeを用いる

ここまでの手順を行っても、生成されたファイルで以下のエラーが発生してしまいます。

// error TS2349: This expression is not callable.
// Type 'typeof import("/.../node_modules/graphql-tag/lib/index")' has no call signatures.
export const ExampleQueryDocument = gql`
  ...

これは、以下のようにgqlをdefault exportとしてimportしようとしているためです。graphql-tagのESMにおいてはgqlはdefault exportではないため、これはエラーとなります。

import gql from 'graphql-tag';

現状では以下のようにDocumentNodeを代わりに使用するようにします。(documentModeとdocumentNodeが紛らわしいので注意してください)

  generates:
    src/hoge/generated.mts:
      plugins:
        - 'typescript'
        - 'typescript-operations'
        - 'typescript-graphql-request'
+     config:
+       documentMode: documentNode

参考: https://github.com/dotansimha/graphql-code-generator-community/issues/228

Discussion