text: const fs = require(`fs-extra`)
  const {
    EnumTypeComposer,
    InputTypeComposer,
    InterfaceTypeComposer,
    ObjectTypeComposer,
    ScalarTypeComposer,
    UnionTypeComposer,
  } = require(`graphql-compose`)
  const report = require(`gatsby-cli/lib/reporter`)
  
  const { internalExtensionNames } = require(`./extensions`)
  
  const printTypeDefinitions = ({ config, schemaComposer }) => {
    if (!config) return Promise.resolve()
  
    const { path, include = {}, exclude = {}, withFieldTypes } = config || {}
  
    if (!path) {
      report.error(
        `Printing type definitions aborted. Please provide a file path.`
      )
      return Promise.resolve()
    }
  
    if (fs.existsSync(path)) {
      report.error(
        `Printing type definitions aborted. The file \`${path}\` already exists.`
      )
      return Promise.resolve()
    }
  
    const internalTypes = [
      `Boolean`,
      `Buffer`,
      `Date`,
      `Float`,
      `ID`,
      `Int`,
      `Internal`,
      `InternalInput`,
      `JSON`,
      `Json`,
      `Node`,
      `NodeInput`,
      `Query`,
      `String`,
    ]
    const internalPlugins = [`internal-data-bridge`]
  
    const typesToExclude = exclude.types || []
    const pluginsToExclude = exclude.plugins || []
  
    const getName = tc => tc.name || tc.getTypeName()
  
    const isInternalType = tc => {
      const typeName = getName(tc)
      if (internalTypes.includes(typeName)) {
        return true
      }
  
      const plugin = tc.getExtension(`plugin`)
      if (internalPlugins.includes(plugin)) {
        return true
      }
  
      return false
    }
  
    const shouldIncludeType = tc => {
      const typeName = getName(tc)
      if (typesToExclude.includes(typeName)) {
        return false
      }
      if (include.types && !include.types.includes(typeName)) {
        return false
      }
  
      const plugin = tc.getExtension(`plugin`)
      if (pluginsToExclude.includes(plugin)) {
        return false
      }
      if (include.plugins && !include.plugins.includes(plugin)) {
        return false
      }
  
      return true
    }
  
    // Save processed type names, not references to the type composers,
    // because of how graphql-compose, at least in v6, processes
    // inline types
    const processedTypes = new Set()
    const typeDefs = new Set()
  
    const addType = tc => {
      const typeName = getName(tc)
      if (!processedTypes.has(typeName) && !isInternalType(tc)) {
        processedTypes.add(typeName)
        return typeDefs.add(tc)
      }
      processedTypes.add(typeName)
      return null
    }
  
    const addWithFieldTypes = tc => {
      if (
        addType(tc) &&
        (tc instanceof ObjectTypeComposer ||
          tc instanceof InterfaceTypeComposer ||
          tc instanceof InputTypeComposer)
      ) {
        if (tc instanceof ObjectTypeComposer) {
          const interfaces = tc.getInterfaces()
          interfaces.forEach(iface => {
            const ifaceName = getName(iface)
            if (ifaceName !== `Node`) {
              addWithFieldTypes(schemaComposer.getAnyTC(ifaceName))
            }
          })
        }
  
        tc.getFieldNames().forEach(fieldName => {
          const fieldType = tc.getFieldTC(fieldName)
          addWithFieldTypes(fieldType)
  
          if (!(tc instanceof InputTypeComposer)) {
            const fieldArgs = tc.getFieldArgs(fieldName)
            Object.keys(fieldArgs).forEach(argName => {
              addWithFieldTypes(tc.getFieldArgTC(fieldName, argName))
            })
          }
        })
      }
    }
  
    schemaComposer.forEach(tc => {
      if (!isInternalType(tc) && shouldIncludeType(tc)) {
        if (withFieldTypes) {
          addWithFieldTypes(tc)
        } else {
          addType(tc)
        }
      }
    })
  
    const printedTypeDefs = [
      `### Type definitions saved at ${new Date().toISOString()} ###`,
    ]
  
    try {
      typeDefs.forEach(tc => printedTypeDefs.push(printType(tc)))
      report.info(`Writing GraphQL type definitions to ${path}`)
      return fs.writeFile(path, printedTypeDefs.join(`\n\n`))
    } catch (error) {
      report.error(`Failed writing type definitions to \`${path}\`.`, error)
      return Promise.resolve()
    }
  }
  
  const printType = (tc, typeName) => {
    if (tc instanceof ObjectTypeComposer) {
      return printObjectType(tc)
    } else if (tc instanceof InterfaceTypeComposer) {
      return printInterfaceType(tc)
    } else if (tc instanceof UnionTypeComposer) {
      return printUnionType(tc)
    } else if (tc instanceof EnumTypeComposer) {
      return printEnumType(tc)
    } else if (tc instanceof ScalarTypeComposer) {
      return printScalarType(tc)
    } else if (tc instanceof InputTypeComposer) {
      return printInputObjectType(tc)
    } else {
      throw new Error(`Did not recognize type of ${typeName}.`)
    }
  }
  
  // ------------------------- graphql-js schemaPrinter -------------------------
  
  const {
    astFromValue,
    print,
    GraphQLString,
    DEFAULT_DEPRECATION_REASON,
  } = require(`graphql`)
  const { printBlockString } = require(`graphql/language/blockString`)
  const { flatMap } = require(`graphql/polyfills/flatMap`)
  
  const printScalarType = tc => {
    const type = tc.getType()
    return printDescription(type) + `scalar ${type.name}`
  }
  
  const printObjectType = tc => {
    const type = tc.getType()
    const interfaces = type.getInterfaces()
    const implementedInterfaces = interfaces.length
      ? ` implements ` + interfaces.map(i => i.name).join(` & `)
      : ``
    const extensions = tc.getExtensions()
    if (tc.hasInterface(`Node`)) {
      extensions.dontInfer = null
    }
    const directives = tc.schemaComposer.getDirectives()
    const printedDirectives = printDirectives(extensions, directives)
    const fields = tc.hasInterface(`Node`)
      ? Object.values(type.getFields()).filter(
          field => ![`id`, `parent`, `children`, `internal`].includes(field.name)
        )
      : Object.values(type.getFields())
    return (
      printDescription(type) +
      `type ${type.name}${implementedInterfaces}${printedDirectives}` +
      printFields(fields, directives)
    )
  }
  
  const printInterfaceType = tc => {
    const type = tc.getType()
    const extensions = tc.getExtensions()
    const directives = tc.schemaComposer.getDirectives()
    const printedDirectives = printDirectives(extensions, directives)
    return (
      printDescription(type) +
      `interface ${type.name}${printedDirectives}` +
      printFields(Object.values(type.getFields()), directives)
    )
  }
  
  const printUnionType = tc => {
    const type = tc.getType()
    const types = type.getTypes()
    const possibleTypes = types.length ? ` = ` + types.join(` | `) : ``
    return printDescription(type) + `union ` + type.name + possibleTypes
  }
  
  const printEnumType = tc => {
    const type = tc.getType()
    const values = type
      .getValues()
      .map(
        (value, i) =>
          printDescription(value, `  `, !i) +
          `  ` +
          value.name +
          printDeprecated(value)
      )
  
    return printDescription(type) + `enum ${type.name}` + printBlock(values)
  }
  
  const printInputObjectType = tc => {
    const type = tc.getType()
    const fields = Object.values(type.getFields()).map(
      (f, i) => printDescription(f, `  `, !i) + `  ` + printInputValue(f)
    )
    return printDescription(type) + `input ${type.name}` + printBlock(fields)
  }
  
  const printFields = (fields, directives) => {
    const printedFields = fields.map(
      (f, i) =>
        printDescription(f, `  `, !i) +
        `  ` +
        f.name +
        printArgs(f.args, `  `) +
        `: ` +
        String(f.type) +
        printDirectives(f.extensions || {}, directives) +
        printDeprecated(f)
    )
    return printBlock(printedFields)
  }
  
  const printBlock = items =>
    items.length !== 0 ? ` {\n` + items.join(`\n`) + `\n}` : ``
  
  const printArgs = (args, indentation = ``) => {
    if (args.length === 0) {
      return ``
    }
  
    // If all args have no description, print them on one line
    if (args.every(arg => !arg.description)) {
      return `(` + args.map(printInputValue).join(`, `) + `)`
    }
  
    return (
      `(\n` +
      args
        .map(
          (arg, i) =>
            printDescription(arg, `  ` + indentation, !i) +
            `  ` +
            indentation +
            printInputValue(arg)
        )
        .join(`\n`) +
      `\n` +
      indentation +
      `)`
    )
  }
  
  const printInputValue = arg => {
    const defaultAST = astFromValue(arg.defaultValue, arg.type)
    let argDecl = arg.name + `: ` + String(arg.type)
    if (defaultAST) {
      argDecl += ` = ${print(defaultAST)}`
    }
    return argDecl
  }
  
  const printDirectives = (extensions, directives) =>
    Object.entries(extensions)
      .map(([name, args]) => {
        if ([...internalExtensionNames, `deprecated`].includes(name)) return ``
        return (
          ` @${name}` +
          printDirectiveArgs(
            args,
            directives.find(directive => directive.name === name)
          )
        )
      })
      .join(``)
  
  const printDirectiveArgs = (args, directive) => {
    if (!args || !directive) {
      return ``
    }
  
    const directiveArgs = Object.entries(args)
    if (directiveArgs.length === 0) {
      return ``
    }
  
    return (
      `(` +
      directiveArgs
        .map(([name, value]) => {
          const arg =
            directive.args && directive.args.find(arg => arg.name === name)
          return arg && `${name}: ${print(astFromValue(value, arg.type))}`
        })
        .join(`, `) +
      `)`
    )
  }
  
  const printDeprecated = fieldOrEnumVal => {
    if (!fieldOrEnumVal.isDeprecated) {
      return ``
    }
    const reason = fieldOrEnumVal.deprecationReason
    const reasonAST = astFromValue(reason, GraphQLString)
    if (reasonAST && reason !== `` && reason !== DEFAULT_DEPRECATION_REASON) {
      return ` @deprecated(reason: ` + print(reasonAST) + `)`
    }
    return ` @deprecated`
  }
  
  const printDescription = (def, indentation = ``, firstInBlock = true) => {
    if (!def.description) {
      return ``
    }
  
    const lines = descriptionLines(def.description, 120 - indentation.length)
  
    const text = lines.join(`\n`)
    const preferMultipleLines = text.length > 70
    const blockString = printBlockString(text, ``, preferMultipleLines)
    const prefix = indentation && !firstInBlock ? `\n` + indentation : indentation
  
    return prefix + blockString.replace(/\n/g, `\n` + indentation) + `\n`
  }
  
  const descriptionLines = (description, maxLen) => {
    const rawLines = description.split(`\n`)
    return flatMap(rawLines, line => {
      if (line.length < maxLen + 5) {
        return line
      }
      // For > 120 character long lines, cut at space boundaries into sublines
      // of ~80 chars.
      return breakLine(line, maxLen)
    })
  }
  
  const breakLine = (line, maxLen) => {
    const parts = line.split(new RegExp(`((?: |^).{15,${maxLen - 40}}(?= |$))`))
    if (parts.length < 4) {
      return [line]
    }
    const sublines = [parts[0] + parts[1] + parts[2]]
    for (let i = 3; i < parts.length; i += 2) {
      sublines.push(parts[i].slice(1) + parts[i + 1])
    }
    return sublines
  }
  
  module.exports = {
    printTypeDefinitions,
  }
  