Files
30-seconds-of-code/node_modules/@gatsbyjs/relay-compiler/lib/SkipRedundantNodesTransform.js
2019-08-20 15:52:05 +02:00

218 lines
5.7 KiB
JavaScript

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*
* @format
*/
'use strict';
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread"));
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
var GraphQLCompilerContext = require("./GraphQLCompilerContext");
var GraphQLIRTransformer = require("./GraphQLIRTransformer");
var IMap = require("immutable").Map;
var getIdentifierForSelection = require("./getIdentifierForSelection");
var invariant = require("fbjs/lib/invariant");
/**
* A transform that removes redundant fields and fragment spreads. Redundancy is
* defined in this context as any selection that is guaranteed to already be
* fetched by an ancestor selection. This can occur in two cases:
*
* 1. Simple duplicates at the same level of the document can always be skipped:
*
* ```
* fragment Foo on FooType {
* id
* id
* ...Bar
* ...Bar
* }
* ```
*
* Becomes
*
* ```
* fragment Foo on FooType {
* id
* ...Bar
* }
* ```
*
* 2. Inline fragments and conditions introduce the possibility for duplication
* at different levels of the tree. Whenever a selection is fetched in a parent,
* it is redundant to also fetch it in a child:
*
* ```
* fragment Foo on FooType {
* id
* ... on OtherType {
* id # 1
* }
* ... on FooType @include(if: $cond) {
* id # 2
* }
* }
* ```
*
* Becomes:
*
* ```
* fragment Foo on FooType {
* id
* }
* ```
*
* In this example:
* - 1 can be skipped because `id` is already fetched by the parent. Even
* though the type is different (FooType/OtherType), the inline fragment
* cannot match without the outer fragment matching so the outer `id` is
* guaranteed to already be fetched.
* - 2 can be skipped for similar reasons: it doesn't matter if the condition
* holds, `id` is already fetched by the parent regardless.
*
* This transform also handles more complicated cases in which selections are
* nested:
*
* ```
* fragment Foo on FooType {
* a {
* bb
* }
* ... on OtherType {
* a {
* bb # 1
* cc
* }
* }
* }
* ```
*
* Becomes
*
* ```
* fragment Foo on FooType {
* a {
* bb
* }
* ... on OtherType {
* a {
* cc
* }
* }
* }
* ```
*
* 1 can be skipped because it is already fetched at the outer level.
*/
function skipRedundantNodesTransform(context) {
return GraphQLIRTransformer.transform(context, {
Root: visitNode,
Fragment: visitNode
});
}
function visitNode(node) {
return transformNode(node, new IMap()).node;
}
/**
* The most straightforward approach would be two passes: one to record the
* structure of the document, one to prune duplicates. This implementation uses
* a single pass. Selections are sorted with fields first, "conditionals"
* (inline fragments & conditions) last. This means that all fields that are
* guaranteed to be fetched are encountered prior to any duplicates that may be
* fetched within a conditional.
*
* Because selections fetched within a conditional are not guaranteed to be
* fetched in the parent, a fork of the selection map is created when entering a
* conditional. The sort ensures that guaranteed fields have already been seen
* prior to the clone.
*/
function transformNode(node, selectionMap) {
var selections = [];
sortSelections(node.selections).forEach(function (selection) {
var identifier = getIdentifierForSelection(selection);
switch (selection.kind) {
case 'ScalarField':
case 'FragmentSpread':
{
if (!selectionMap.has(identifier)) {
selections.push(selection);
selectionMap = selectionMap.set(identifier, null);
}
break;
}
case 'Defer':
case 'Stream':
case 'MatchBranch':
case 'MatchField':
case 'LinkedField':
{
var transformed = transformNode(selection, selectionMap.get(identifier) || new IMap());
if (transformed.node) {
selections.push(transformed.node);
selectionMap = selectionMap.set(identifier, transformed.selectionMap);
}
break;
}
case 'InlineFragment':
case 'Condition':
{
// Fork the selection map to prevent conditional selections from
// affecting the outer "guaranteed" selections.
var _transformed = transformNode(selection, selectionMap.get(identifier) || selectionMap);
if (_transformed.node) {
selections.push(_transformed.node);
selectionMap = selectionMap.set(identifier, _transformed.selectionMap);
}
break;
}
default:
selection;
!false ? process.env.NODE_ENV !== "production" ? invariant(false, 'SkipRedundantNodesTransform: Unexpected node kind `%s`.', selection.kind) : invariant(false) : void 0;
}
});
var nextNode = selections.length ? (0, _objectSpread2["default"])({}, node, {
selections: selections
}) : null;
return {
selectionMap: selectionMap,
node: nextNode
};
}
/**
* Sort inline fragments and conditions after other selections.
*/
function sortSelections(selections) {
return (0, _toConsumableArray2["default"])(selections).sort(function (a, b) {
return a.kind === 'InlineFragment' || a.kind === 'Condition' ? 1 : b.kind === 'InlineFragment' || b.kind === 'Condition' ? -1 : 0;
});
}
module.exports = {
transform: skipRedundantNodesTransform
};