graphql-tools でコード中の GraphQL Query を抜き出す

フロントエンドで以下のようにコード中に GraphQL を書いたとする。

```code-with-queries.js
const queryA = graphql(`
query QueryA {
  viewer {
    ...FragmentA
  }
}
`)

const fragmentA = graphql(`
query FragmentA on Viewer {
  id
  name
  ...FragmentB
}
`)

const fragmentB = graphql(`
query FragmentB on Viewer {
  followers(first: 3) {
    node {
      id
      name
    }
  }
}
`)
```

ここで書いた Query を諸事情で、以下のように抜き出して利用したいことがある。

```QueryA-and-dependencies.gql
# QueryA と利用している Fragment のみを抜き出す。
query QueryA {
  viewer {
    ...FragmentA
  }
}

query FragmentA on Viewer {
  id
  name
  ...FragmentB
}

query FragmentB on Viewer {
  followers(first: 3) {
    node {
      id
      name
    }
  }
}
```

こういうことは、 [GraphQL-Tools](https://the-guild.dev/graphql/tools) の loader を使えば行える。

https://the-guild.dev/graphql/tools/docs/documents-loading

今回やりたいことを実際に行うサンプルコードから用意したので、これを元に解説する。

https://gist.github.com/tomoasleep/4eb4de1bc5abc8adba18dd7fef9139dc

## 1. `@graphql-tools/load`, `@graphql-tools/xxx-loader` を使って GraphQL Operation を抜き出せる

graphql-tools の loader を使って、以下のように GraphQL コードを抜き出せる。

https://the-guild.dev/graphql/tools/docs/documents-loading

```js
import { loadDocuments } from '@graphql-tools/load'

// 読み込む元のファイルの種類ごとの loader が用意されている
import { CodeFileLoader } from '@graphql-tools/code-file-loader'

async function main() {
  const sources = await loadDocuments('./src/**/*.{ts,tsx}', {
    loaders: [new CodeFileLoader()],
    pluckConfig: {
      globalGqlIdentifierName: 'graphql',
    },
  })

  // ...
}
```

## 2. loader した GraphQL コードの AST を走査する

読み込んだコードは graphql-tools の [Source](https://github.com/ardatan/graphql-tools/blob/2fe23beed5d1b9a0058ed64791bbf6e175072ab7/packages/utils/src/loaders.ts#L4-L9) として返されて、この中にコードの文字列やファイル名と GraphQL コードの AST が含まれている。

この AST は [graphql.js の Visitor API](https://graphql.org/graphql-js/language/#visitor-1) を使って走査出来る。

```js
import { visit } from 'graphql'

/** @typedef {import("@graphql-tools/utils").Source} Source */
/** @typedef {import("graphql").ExecutableDefinitionNode} ExecutableDefinitionNode */

class DocumentDependencyGraph {
  /**
   * @param {Source[]} sources
   * @return {SchemaDependencyGraph}
   */
  static fromSources(sources) {
    const graph = new DocumentDependencyGraph()

    sources.forEach(source => {
      const document = source.document
      let currentName = null

      visit(document, {
        // 各 query, mutation 宣言それぞれに対して呼び出す
        OperationDefinition(node) {
          if (node.name) {
            currentName = node.name.value
            graph.addDefinition(node.name.value, node)
          }
        },

        // 各 fragment 宣言それぞれに対して呼び出す
        FragmentDefinition(node) {
          currentName = node.name.value
          graph.addDefinition(node.name.value, node)
        },

        // `...Fragment` のような fragment を展開する箇所それぞれで呼び出す
        FragmentSpread(node) {
          if (currentName) {
            graph.addDependency(currentName, node.name.value)
          }
        },
      })
    })

    return graph
  }
  
  constructor() {
    /** @type {Object<string, ExecutableDefinitionNode>} */
    this.definitions = {}

    /** @type {Object<string, Set<string>>} */
    this.dependencies = {}
  }
  
  // ...
}
```

AST の各 Node の定義は ↓ にまとまっているのでこれらを参照。

https://github.com/graphql/graphql-js/blob/16.x.x/src/language/ast.ts

自分が見た感じ、 各 Node の定義は GraphQL の Spec 内の名前とほぼ同じなので、こちらを参考にしてもよいかも。

https://spec.graphql.org/draft/#sec-Language

## 3. print 

走査した GraphQL AST は [GraphQL.js の `print` 関数](https://graphql.org/graphql-js/language/#printer-1)  で文字列として出力出来る。

これで、関連する fragment を含めた GraphQL operation の定義を出力できる。

```js
import { print } from 'graphql'

  /**
   * @param {string} name
   * @return {string}
   */
  getSourceWithDependencies(name) {
    const source = print(this.definitions[name])
    const dependencies = this.getDependencies(name)
    return (
      dependencies.reduce((source, dependency) => {
        const dependencySource = this.definitions[dependency]

        return source + '\n\n' + print(dependencySource)
      }, source) + '\n'
    )
  }
```