text: // @flow
  const fs = require(`fs-extra`)
  const crypto = require(`crypto`)
  const _ = require(`lodash`)
  const slugify = require(`slugify`)
  
  // Traverse is a es6 module...
  import traverse from "@babel/traverse"
  const {
    getGraphQLTag,
    StringInterpolationNotAllowedError,
    EmptyGraphQLTagError,
    GraphQLSyntaxError,
  } = require(`babel-plugin-remove-graphql-queries`)
  
  const report = require(`gatsby-cli/lib/reporter`)
  
  import type { DocumentNode, DefinitionNode } from "graphql"
  import { babelParseToAst } from "../utils/babel-parse-to-ast"
  import { codeFrameColumns } from "@babel/code-frame"
  
  const apiRunnerNode = require(`../utils/api-runner-node`)
  const { boundActionCreators } = require(`../redux/actions`)
  import { locInGraphQlToLocInFile } from "./error-parser"
  /**
   * Add autogenerated query name if it wasn't defined by user.
   */
  const generateQueryName = ({ def, hash, file }) => {
    if (!def.name || !def.name.value) {
      const slugified = slugify(file, {
        replacement: ` `,
        lower: false,
      })
      def.name = {
        value: `${_.camelCase(slugified)}${hash}`,
        kind: `Name`,
      }
    }
    return def
  }
  
  const warnForUnknownQueryVariable = (varName, file, usageFunction) =>
    report.warn(
      `\nWe were unable to find the declaration of variable "${varName}", which you passed as the "query" prop into the ${usageFunction} declaration in "${file}".
  
  Perhaps the variable name has a typo?
  
  Also note that we are currently unable to use queries defined in files other than the file where the ${usageFunction} is defined. If you're attempting to import the query, please move it into "${file}". If being able to import queries from another file is an important capability for you, we invite your help fixing it.\n`
    )
  
  async function parseToAst(filePath, fileStr, { parentSpan, addError } = {}) {
    let ast
  
    // Preprocess and attempt to parse source; return an AST if we can, log an
    // error if we can't.
    const transpiled = await apiRunnerNode(`preprocessSource`, {
      filename: filePath,
      contents: fileStr,
      parentSpan: parentSpan,
    })
    if (transpiled && transpiled.length) {
      for (const item of transpiled) {
        try {
          const tmp = babelParseToAst(item, filePath)
          ast = tmp
          break
        } catch (error) {
          boundActionCreators.queryExtractionGraphQLError({
            componentPath: filePath,
          })
          continue
        }
      }
      if (ast === undefined) {
        addError({
          id: `85912`,
          filePath,
          context: {
            filePath,
          },
        })
        boundActionCreators.queryExtractionGraphQLError({
          componentPath: filePath,
        })
  
        return null
      }
    } else {
      try {
        ast = babelParseToAst(fileStr, filePath)
      } catch (error) {
        boundActionCreators.queryExtractionBabelError({
          componentPath: filePath,
          error,
        })
  
        addError({
          id: `85911`,
          filePath,
          context: {
            filePath,
          },
        })
  
        return null
      }
    }
  
    return ast
  }
  
  const warnForGlobalTag = file =>
    report.warn(
      `Using the global \`graphql\` tag is deprecated, and will not be supported in v3.\n` +
        `Import it instead like:  import { graphql } from 'gatsby' in file:\n` +
        file
    )
  
  async function findGraphQLTags(
    file,
    text,
    { parentSpan, addError } = {}
  ): Promise<Array<DefinitionNode>> {
    return new Promise((resolve, reject) => {
      parseToAst(file, text, { parentSpan, addError })
        .then(ast => {
          let queries = []
          if (!ast) {
            resolve(queries)
            return
          }
  
          /**
           * A map of graphql documents to unique locations.
           *
           * A graphql document's unique location is made of:
           *
           *  - the location of the graphql template literal that contains the document, and
           *  - the document's location within the graphql template literal
           *
           * This is used to prevent returning duplicated documents.
           */
          const documentLocations = new WeakMap()
  
          const extractStaticQuery = (
            taggedTemplateExpressPath,
            isHook = false
          ) => {
            const { ast: gqlAst, text, hash, isGlobal } = getGraphQLTag(
              taggedTemplateExpressPath
            )
            if (!gqlAst) return
  
            if (isGlobal) warnForGlobalTag(file)
  
            gqlAst.definitions.forEach(def => {
              documentLocations.set(
                def,
                `${taggedTemplateExpressPath.node.start}-${def.loc.start}`
              )
              generateQueryName({
                def,
                hash,
                file,
              })
            })
  
            const definitions = [...gqlAst.definitions].map(d => {
              d.isStaticQuery = true
              d.isHook = isHook
              d.text = text
              d.hash = hash
  
              taggedTemplateExpressPath.traverse({
                TemplateElement(templateElementPath) {
                  d.templateLoc = templateElementPath.node.loc
                },
              })
  
              return d
            })
  
            queries.push(...definitions)
          }
  
          // Look for queries in <StaticQuery /> elements.
          traverse(ast, {
            JSXElement(path) {
              if (path.node.openingElement.name.name !== `StaticQuery`) {
                return
              }
  
              // astexplorer.com link I (@kyleamathews) used when prototyping this algorithm
              // https://astexplorer.net/#/gist/ab5d71c0f08f287fbb840bf1dd8b85ff/2f188345d8e5a4152fe7c96f0d52dbcc6e9da466
              path.traverse({
                JSXAttribute(jsxPath) {
                  if (jsxPath.node.name.name !== `query`) {
                    return
                  }
                  jsxPath.traverse({
                    // Assume the query is inline in the component and extract that.
                    TaggedTemplateExpression(templatePath) {
                      extractStaticQuery(templatePath)
                    },
                    // Also see if it's a variable that's passed in as a prop
                    // and if it is, go find it.
                    Identifier(identifierPath) {
                      if (identifierPath.node.name !== `graphql`) {
                        const varName = identifierPath.node.name
                        let found = false
                        traverse(ast, {
                          VariableDeclarator(varPath) {
                            if (
                              varPath.node.id.name === varName &&
                              varPath.node.init.type ===
                                `TaggedTemplateExpression`
                            ) {
                              varPath.traverse({
                                TaggedTemplateExpression(templatePath) {
                                  found = true
                                  extractStaticQuery(templatePath)
                                },
                              })
                            }
                          },
                        })
                        if (!found) {
                          warnForUnknownQueryVariable(
                            varName,
                            file,
                            `<StaticQuery>`
                          )
                        }
                      }
                    },
                  })
                },
              })
              return
            },
          })
  
          // Look for queries in useStaticQuery hooks.
          traverse(ast, {
            CallExpression(hookPath) {
              if (
                hookPath.node.callee.name !== `useStaticQuery` ||
                !hookPath.get(`callee`).referencesImport(`gatsby`)
              ) {
                return
              }
  
              const firstArg = hookPath.get(`arguments`)[0]
  
              // Assume the query is inline in the component and extract that.
              if (firstArg.isTaggedTemplateExpression()) {
                extractStaticQuery(firstArg, true)
                // Also see if it's a variable that's passed in as a prop
                // and if it is, go find it.
              } else if (firstArg.isIdentifier()) {
                if (
                  firstArg.node.name !== `graphql` &&
                  firstArg.node.name !== `useStaticQuery`
                ) {
                  const varName = firstArg.node.name
                  let found = false
                  traverse(ast, {
                    VariableDeclarator(varPath) {
                      if (
                        varPath.node.id.name === varName &&
                        varPath.node.init.type === `TaggedTemplateExpression`
                      ) {
                        varPath.traverse({
                          TaggedTemplateExpression(templatePath) {
                            found = true
                            extractStaticQuery(templatePath, true)
                          },
                        })
                      }
                    },
                  })
                  if (!found) {
                    warnForUnknownQueryVariable(varName, file, `useStaticQuery`)
                  }
                }
              }
            },
          })
  
          // Look for exported page queries
          traverse(ast, {
            ExportNamedDeclaration(path, state) {
              path.traverse({
                TaggedTemplateExpression(innerPath) {
                  const { ast: gqlAst, isGlobal, hash, text } = getGraphQLTag(
                    innerPath
                  )
                  if (!gqlAst) return
  
                  if (isGlobal) warnForGlobalTag(file)
  
                  gqlAst.definitions.forEach(def => {
                    documentLocations.set(
                      def,
                      `${innerPath.node.start}-${def.loc.start}`
                    )
                    generateQueryName({
                      def,
                      hash,
                      file,
                    })
                  })
  
                  queries.push(
                    ...gqlAst.definitions.map(d => {
                      d.text = text
  
                      innerPath.traverse({
                        TemplateElement(templateElementPath) {
                          d.templateLoc = templateElementPath.node.loc
                        },
                      })
  
                      return d
                    })
                  )
                },
              })
            },
          })
  
          // Remove duplicate queries
          const uniqueQueries = _.uniqBy(queries, q => documentLocations.get(q))
  
          resolve(uniqueQueries)
        })
        .catch(reject)
    })
  }
  
  const cache = {}
  
  export default class FileParser {
    constructor({ parentSpan } = {}) {
      this.parentSpan = parentSpan
    }
  
    async parseFile(file: string, addError): Promise<?DocumentNode> {
      let text
      try {
        text = await fs.readFile(file, `utf8`)
      } catch (err) {
        addError({
          id: `85913`,
          filePath: file,
          context: {
            filePath: file,
          },
          error: err,
        })
  
        boundActionCreators.queryExtractionGraphQLError({
          componentPath: file,
        })
        return null
      }
  
      if (!text.includes(`graphql`)) return null
      const hash = crypto
        .createHash(`md5`)
        .update(file)
        .update(text)
        .digest(`hex`)
  
      try {
        let astDefinitions =
          cache[hash] ||
          (cache[hash] = await findGraphQLTags(file, text, {
            parentSpan: this.parentSpan,
            addError,
          }))
  
        // If any AST definitions were extracted, report success.
        // This can mean there is none or there was a babel error when
        // we tried to extract the graphql AST.
        if (astDefinitions.length > 0) {
          boundActionCreators.queryExtractedBabelSuccess({
            componentPath: file,
          })
        }
  
        return astDefinitions.length
          ? {
              kind: `Document`,
              definitions: astDefinitions,
            }
          : null
      } catch (err) {
        // default error
        let structuredError = {
          id: `85915`,
          context: {
            filePath: file,
          },
        }
  
        if (err instanceof StringInterpolationNotAllowedError) {
          const location = {
            start: err.interpolationStart,
            end: err.interpolationEnd,
          }
          structuredError = {
            id: `85916`,
            location,
            context: {
              codeFrame: codeFrameColumns(text, location, {
                highlightCode: process.env.FORCE_COLOR !== `0`,
              }),
            },
          }
        } else if (err instanceof EmptyGraphQLTagError) {
          const location = err.templateLoc
            ? {
                start: err.templateLoc.start,
                end: err.templateLoc.end,
              }
            : null
  
          structuredError = {
            id: `85917`,
            location,
            context: {
              codeFrame: location
                ? codeFrameColumns(text, location, {
                    highlightCode: process.env.FORCE_COLOR !== `0`,
                  })
                : null,
            },
          }
        } else if (err instanceof GraphQLSyntaxError) {
          const location = {
            start: locInGraphQlToLocInFile(
              err.templateLoc,
              err.originalError.locations[0]
            ),
          }
  
          structuredError = {
            id: `85918`,
            location,
            context: {
              codeFrame: location
                ? codeFrameColumns(text, location, {
                    highlightCode: process.env.FORCE_COLOR !== `0`,
                    message: err.originalError.message,
                  })
                : null,
              sourceMessage: err.originalError.message,
            },
          }
        }
  
        addError({
          ...structuredError,
          filePath: file,
        })
  
        boundActionCreators.queryExtractionGraphQLError({
          componentPath: file,
        })
        return null
      }
    }
  
    async parseFiles(
      files: Array<string>,
      addError
    ): Promise<Map<string, DocumentNode>> {
      const documents = new Map()
  
      return Promise.all(
        files.map(file =>
          this.parseFile(file, addError).then(doc => {
            if (!doc) return
            documents.set(file, doc)
          })
        )
      ).then(() => documents)
    }
  }
  