text: const { visit, visitInParallel, Kind } = require(`gatsby/graphql`);
  const _ = require(`lodash`);
  
  const Prefix = {
    create: index => `gatsby${index}_`,
    parseKey: prefixedKey => {
      const match = /^gatsby([\d]+)_(.*)$/.exec(prefixedKey);
      if (!match || match.length !== 3 || isNaN(Number(match[1])) || !match[2]) {
        throw new Error(`Unexpected data key: ${prefixedKey}`);
      }
      return { index: Number(match[1]), originalKey: match[2] };
    },
  };
  
  /**
   * Merge multiple queries into a single query in such a way that query results
   * can be split and transformed as if they were obtained by running original queries.
   *
   * Merging algorithm involves several transformations:
   *  1. Replace top-level fragment spreads with inline fragments (... on Query {})
   *  2. Add unique aliases to all top-level query fields (including those on inline fragments)
   *  3. Prefix all variable definitions and variable usages
   *  4. Prefix names (and spreads) of fragments with variables
   *  5. Dedupe repeating fragments
   *
   * i.e transform:
   *   [
   *     `query Foo($id: ID!) { foo, bar(id: $id), ...FooQuery }
   *     fragment FooQuery on Query { baz }`,
   *
   *    `query Bar($id: ID!) { foo: baz, bar(id: $id), ... on Query { baz } }`
   *   ]
   * to:
   *   query (
   *     $gatsby1_id: ID!
   *     $gatsby2_id: ID!
   *   ) {
   *     gatsby1_foo: foo,
   *     gatsby1_bar: bar(id: $gatsby1_id)
   *     ... on Query @originalFragment(name: "FooQuery") {
   *       gatsby1_baz: baz
   *     }
   *     gatsby2_foo: baz
   *     gatsby2_bar: bar(id: $gatsby2_id)
   *     ... on Query {
   *       gatsby2_baz: baz
   *     }
   *   }
   *   fragment FooQuery on Query { baz }
   */
  export function merge(queries) {
    const mergedVariables = {};
    const mergedVariableDefinitions = [];
    const mergedSelections = [];
    const mergedFragmentMap = new Map();
  
    queries.forEach((query, index) => {
      const prefixedQuery = prefixQueryParts(Prefix.create(index), query);
  
      prefixedQuery.query.definitions.forEach(def => {
        if (isQueryDefinition(def)) {
          mergedSelections.push(...def.selectionSet.selections);
          mergedVariableDefinitions.push(...(def.variableDefinitions ?? []));
        }
        if (isFragmentDefinition(def)) {
          // Theoretically it is possible to have fragments with the same name but different content
          // in different queries. But Gatsby validation doesn't allow this and we rely on this here
          // One example where this can occur is in gatsby-node or GraphiQL queries
          // (but those are usually not batched)
          mergedFragmentMap.set(def.name.value, def);
        }
      });
      Object.assign(mergedVariables, prefixedQuery.variables);
    });
  
    const mergedQueryDefinition = {
      kind: Kind.OPERATION_DEFINITION,
      operation: `query`,
      variableDefinitions: mergedVariableDefinitions,
      selectionSet: {
        kind: Kind.SELECTION_SET,
        selections: mergedSelections,
      },
    };
  
    return {
      query: {
        kind: Kind.DOCUMENT,
        definitions: [mergedQueryDefinition, ...mergedFragmentMap.values()],
      },
      variables: mergedVariables,
    };
  }
  
  /**
   * Split and transform result of the query produced by the `merge` function
   */
  export function resolveResult(mergedQueryResult) {
    const data = mergedQueryResult.data;
  
    return Object.keys(data).reduce((acc, prefixedKey) => {
      const { index, originalKey } = Prefix.parseKey(prefixedKey);
      if (!acc[index]) acc[index] = { data: {} };
      acc[index].data[originalKey] = data[prefixedKey];
      return acc;
    }, []);
  }
  
  const Visitors = {
    detectFragmentsWithVariables: fragmentsWithVariables => {
      let currentFragmentName;
      return {
        [Kind.FRAGMENT_DEFINITION]: {
          enter: def => {
            currentFragmentName = def.name.value;
          },
          leave: () => {
            currentFragmentName = null;
          },
        },
        [Kind.VARIABLE]: () => {
          if (currentFragmentName) {
            fragmentsWithVariables.add(currentFragmentName);
          }
        },
      };
    },
    prefixVariables: prefix => {
      return {
        [Kind.VARIABLE]: variable => prefixNodeName(variable, prefix),
      };
    },
    prefixFragmentNames: (prefix, fragmentNames) => {
      return {
        [Kind.FRAGMENT_DEFINITION]: def =>
          fragmentNames.has(def.name.value) ? prefixNodeName(def, prefix) : def,
        [Kind.FRAGMENT_SPREAD]: def =>
          fragmentNames.has(def.name.value) ? prefixNodeName(def, prefix) : def,
      };
    },
  };
  
  function prefixQueryParts(prefix, query) {
    let document = aliasTopLevelFields(prefix, query.query);
    const variableNames = Object.keys(query.variables);
  
    if (variableNames.length === 0) {
      return { ...query, query: document };
    }
  
    const fragmentsWithVariables = new Set();
  
    document = visit(
      document,
      visitInParallel([
        // Note: the sequence is important due to how visitInParallel deals with node edits
        Visitors.detectFragmentsWithVariables(fragmentsWithVariables),
        Visitors.prefixVariables(prefix),
      ])
    );
  
    if (fragmentsWithVariables.size > 0) {
      // Prefix all fragments and spreads having variables
      // Sadly, have to do another pass as fragment spreads may occur at any level
      // (fragments with variables are relatively rare though)
      document = visit(document, Visitors.prefixFragmentNames(prefix, fragmentsWithVariables), {
        [Kind.DOCUMENT]: [`definitions`],
        [Kind.OPERATION_DEFINITION]: [`selectionSet`],
        [Kind.FRAGMENT_DEFINITION]: [`selectionSet`],
        [Kind.INLINE_FRAGMENT]: [`selectionSet`],
        [Kind.FIELD]: [`selectionSet`],
        [Kind.SELECTION_SET]: [`selections`],
      });
    }
  
    const prefixedVariables = variableNames.reduce((acc, name) => {
      acc[prefix + name] = query.variables[name];
      return acc;
    }, {});
  
    return {
      query: document,
      variables: prefixedVariables,
    };
  }
  
  /**
   * Adds prefixed aliases to top-level fields of the query.
   *
   * @see aliasFieldsInSelection for implementation details
   */
  function aliasTopLevelFields(prefix, doc) {
    const transformer = {
      [Kind.OPERATION_DEFINITION]: def => {
        const { selections } = def.selectionSet;
        return {
          ...def,
          selectionSet: {
            ...def.selectionSet,
            selections: aliasFieldsInSelection(prefix, selections, doc),
          },
        };
      },
    };
    return visit(doc, transformer, { [Kind.DOCUMENT]: [`definitions`] });
  }
  
  /**
   * Add aliases to fields of the selection, including top-level fields of inline fragments.
   * Fragment spreads are converted to inline fragments and their top-level fields are also aliased.
   *
   * Note that this method is shallow. It adds aliases only to the top-level fields and doesn't
   * descend to field sub-selections.
   *
   * For example, transforms:
   *   {
   *     foo
   *     ... on Query { foo }
   *     ...FragmentWithBarField
   *   }
   * To:
   *   {
   *     gatsby1_foo: foo
   *     ... on Query { gatsby1_foo: foo }
   *     ... on Query { gatsby1_bar: bar }
   *   }
   */
  function aliasFieldsInSelection(prefix, selections, document) {
    return _.flatMap(selections, selection => {
      switch (selection.kind) {
        case Kind.INLINE_FRAGMENT:
          return [aliasFieldsInInlineFragment(prefix, selection, document)];
        case Kind.FRAGMENT_SPREAD: {
          const inlineFragment = inlineFragmentSpread(selection, document);
          return [
            addSkipDirective(selection),
            aliasFieldsInInlineFragment(prefix, inlineFragment, document),
          ];
        }
        case Kind.FIELD:
        default:
          return [aliasField(selection, prefix)];
      }
    });
  }
  
  function addSkipDirective(node) {
    const skipDirective = {
      kind: Kind.DIRECTIVE,
      name: { kind: Kind.NAME, value: `skip` },
      arguments: [
        {
          kind: Kind.ARGUMENT,
          name: { kind: Kind.NAME, value: `if` },
          value: { kind: Kind.BOOLEAN, value: true },
        },
      ],
    };
    return {
      ...node,
      directives: [skipDirective],
    };
  }
  
  /**
   * Add aliases to top-level fields of the inline fragment.
   * Returns new inline fragment node.
   *
   * For Example, transforms:
   *   ... on Query { foo, ... on Query { bar: foo } }
   * To
   *   ... on Query { gatsby1_foo: foo, ... on Query { gatsby1_bar: foo } }
   */
  function aliasFieldsInInlineFragment(prefix, fragment, document) {
    const { selections } = fragment.selectionSet;
    return {
      ...fragment,
      selectionSet: {
        ...fragment.selectionSet,
        selections: aliasFieldsInSelection(prefix, selections, document),
      },
    };
  }
  
  /**
   * Replaces fragment spread with inline fragment
   *
   * Example:
   *   query { ...Spread }
   *   fragment Spread on Query { bar }
   *
   * Transforms to:
   *   query { ... on Query { bar } }
   */
  function inlineFragmentSpread(spread, document) {
    const fragment = document.definitions.find(
      def => def.kind === Kind.FRAGMENT_DEFINITION && def.name.value === spread.name.value
    );
    if (!fragment) {
      throw new Error(`Fragment ${spread.name.value} does not exist`);
    }
    const { typeCondition, selectionSet } = fragment;
    return {
      kind: Kind.INLINE_FRAGMENT,
      typeCondition,
      selectionSet,
      directives: spread.directives,
    };
  }
  
  function prefixNodeName(namedNode, prefix) {
    return {
      ...namedNode,
      name: {
        ...namedNode.name,
        value: prefix + namedNode.name.value,
      },
    };
  }
  
  /**
   * Returns a new FieldNode with prefixed alias
   *
   * Example. Given prefix === "gatsby1_" transforms:
   *   { foo } -> { gatsby1_foo: foo }
   *   { foo: bar } -> { gatsby1_foo: bar }
   */
  function aliasField(field, aliasPrefix) {
    const aliasNode = field.alias ? field.alias : field.name;
    return {
      ...field,
      alias: {
        ...aliasNode,
        value: aliasPrefix + aliasNode.value,
      },
    };
  }
  
  function isQueryDefinition(def) {
    return def.kind === Kind.OPERATION_DEFINITION && def.operation === `query`;
  }
  
  function isFragmentDefinition(def) {
    return def.kind === Kind.FRAGMENT_DEFINITION;
  }
  