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

330 lines
11 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 IRTransformer = require("./GraphQLIRTransformer");
var RelayCompilerScope = require("./RelayCompilerScope");
var getIdentifierForArgumentValue = require("./getIdentifierForArgumentValue");
var murmurHash = require("./murmurHash");
var _require = require("./RelayCompilerError"),
createCompilerError = _require.createCompilerError,
createNonRecoverableUserError = _require.createNonRecoverableUserError;
var getFragmentScope = RelayCompilerScope.getFragmentScope,
getRootScope = RelayCompilerScope.getRootScope;
/**
* A tranform that converts a set of documents containing fragments/fragment
* spreads *with* arguments to one where all arguments have been inlined. This
* is effectively static currying of functions. Nodes are changed as follows:
* - Fragment spreads with arguments are replaced with references to an inlined
* version of the referenced fragment.
* - Fragments with argument definitions are cloned once per unique set of
* arguments, with the name changed to original name + hash and all nested
* variable references changed to the value of that variable given its
* arguments.
* - Field & directive argument variables are replaced with the value of those
* variables in context.
* - All nodes are cloned with updated children.
*
* The transform also handles statically passing/failing Condition nodes:
* - Literal Conditions with a passing value are elided and their selections
* inlined in their parent.
* - Literal Conditions with a failing value are removed.
* - Nodes that would become empty as a result of the above are removed.
*
* Note that unreferenced fragments are not added to the output.
*/
function relayApplyFragmentArgumentTransform(context) {
var fragments = new Map();
var nextContext = IRTransformer.transform(context, {
Root: function Root(node) {
var scope = getRootScope(node.argumentDefinitions);
return transformNode(context, fragments, scope, node, [node]);
},
// Fragments are included below where referenced.
// Unreferenced fragments are not included.
Fragment: function Fragment() {
return null;
}
});
return Array.from(fragments.values()).reduce(function (ctx, fragment) {
return fragment ? ctx.add(fragment) : ctx;
}, nextContext);
}
function transformNode(context, fragments, scope, node, errorContext) {
var selections = transformSelections(context, fragments, scope, node.selections, errorContext);
if (!selections) {
return null;
}
if (node.hasOwnProperty('directives')) {
var directives = transformDirectives(scope, node.directives, errorContext); // $FlowIssue: this is a valid `Node`:
return (0, _objectSpread2["default"])({}, node, {
directives: directives,
selections: selections
});
}
return (0, _objectSpread2["default"])({}, node, {
selections: selections
});
}
function transformFragmentSpread(context, fragments, scope, spread, errorContext) {
var directives = transformDirectives(scope, spread.directives, errorContext);
var appliedFragment = transformFragment(context, fragments, scope, spread, spread.args, (0, _toConsumableArray2["default"])(errorContext).concat([spread]));
if (!appliedFragment) {
return null;
}
var transformed = (0, _objectSpread2["default"])({}, spread, {
kind: 'FragmentSpread',
args: [],
directives: directives,
name: appliedFragment.name
});
return transformed;
}
function transformField(context, fragments, scope, field, errorContext) {
var args = transformArguments(scope, field.args, errorContext);
var directives = transformDirectives(scope, field.directives, errorContext);
if (field.kind === 'LinkedField' || field.kind === 'MatchField') {
var selections = transformSelections(context, fragments, scope, field.selections, errorContext);
if (!selections) {
return null;
} // $FlowFixMe(>=0.28.0)
return (0, _objectSpread2["default"])({}, field, {
args: args,
directives: directives,
selections: selections
});
} else {
return (0, _objectSpread2["default"])({}, field, {
args: args,
directives: directives
});
}
}
function transformCondition(context, fragments, scope, node, errorContext) {
var condition = transformValue(scope, node.condition, errorContext);
if (!(condition.kind === 'Literal' || condition.kind === 'Variable')) {
// This transform does whole-program optimization, errors in
// a single document could break invariants and/or cause
// additional spurious errors.
throw createNonRecoverableUserError('A non-scalar value was applied to an @include or @skip directive, ' + 'the `if` argument value must be a ' + 'variable or a literal Boolean.', [condition.loc]);
}
if (condition.kind === 'Literal' && condition.value !== node.passingValue) {
// Dead code, no need to traverse further.
return null;
}
var selections = transformSelections(context, fragments, scope, node.selections, errorContext);
if (!selections) {
return null;
}
if (condition.kind === 'Literal' && condition.value === node.passingValue) {
// Always passes, return inlined selections
return selections;
}
return [(0, _objectSpread2["default"])({}, node, {
condition: condition,
selections: selections
})];
}
function transformSelections(context, fragments, scope, selections, errorContext) {
var nextSelections = null;
selections.forEach(function (selection) {
var nextSelection;
if (selection.kind === 'InlineFragment' || selection.kind === 'MatchBranch') {
nextSelection = transformNode(context, fragments, scope, selection, errorContext);
} else if (selection.kind === 'FragmentSpread') {
nextSelection = transformFragmentSpread(context, fragments, scope, selection, errorContext);
} else if (selection.kind === 'Condition') {
var conditionSelections = transformCondition(context, fragments, scope, selection, errorContext);
if (conditionSelections) {
var _nextSelections;
nextSelections = nextSelections || [];
(_nextSelections = nextSelections).push.apply(_nextSelections, (0, _toConsumableArray2["default"])(conditionSelections));
}
} else if (selection.kind === 'LinkedField' || selection.kind === 'ScalarField' || selection.kind === 'MatchField') {
nextSelection = transformField(context, fragments, scope, selection, errorContext);
} else if (selection.kind === 'Defer' || selection.kind === 'Stream') {
throw createCompilerError('RelayApplyFragmentArgumentTransform: Expected to be applied before processing @defer/@stream.', [selection.loc]);
} else {
selection;
throw createCompilerError("RelayApplyFragmentArgumentTransform: Unsupported kind '".concat(selection.kind, "'."), [selection.loc]);
}
if (nextSelection) {
nextSelections = nextSelections || [];
nextSelections.push(nextSelection);
}
});
return nextSelections;
}
function transformDirectives(scope, directives, errorContext) {
return directives.map(function (directive) {
var args = transformArguments(scope, directive.args, errorContext);
return (0, _objectSpread2["default"])({}, directive, {
args: args
});
});
}
function transformArguments(scope, args, errorContext) {
return args.map(function (arg) {
var value = transformValue(scope, arg.value, errorContext);
return value === arg.value ? arg : (0, _objectSpread2["default"])({}, arg, {
value: value
});
});
}
function transformValue(scope, value, errorContext) {
if (value.kind === 'Variable') {
var scopeValue = scope[value.variableName];
if (scopeValue == null) {
// This transform does whole-program optimization, errors in
// a single document could break invariants and/or cause
// additional spurious errors.
throw createNonRecoverableUserError("Variable '$".concat(value.variableName, "' is not in scope."), [value.loc]);
}
return scopeValue;
} else if (value.kind === 'ListValue') {
return (0, _objectSpread2["default"])({}, value, {
items: value.items.map(function (item) {
return transformValue(scope, item, errorContext);
})
});
} else if (value.kind === 'ObjectValue') {
return (0, _objectSpread2["default"])({}, value, {
fields: value.fields.map(function (field) {
return (0, _objectSpread2["default"])({}, field, {
value: transformValue(scope, field.value, errorContext)
});
})
});
}
return value;
}
/**
* Apply arguments to a fragment, creating a new fragment (with the given name)
* with all values recursively applied.
*/
function transformFragment(context, fragments, parentScope, spread, args, errorContext) {
var fragment = context.getFragment(spread.name);
var argumentsHash = hashArguments(args, parentScope, errorContext);
var fragmentName = argumentsHash ? "".concat(fragment.name, "_").concat(argumentsHash) : fragment.name;
var appliedFragment = fragments.get(fragmentName);
if (appliedFragment) {
return appliedFragment;
}
var fragmentScope = getFragmentScope(fragment.argumentDefinitions, args, parentScope, spread);
if (fragments.get(fragmentName) === null) {
// This transform does whole-program optimization, errors in
// a single document could break invariants and/or cause
// additional spurious errors.
throw createNonRecoverableUserError("Found a circular reference from fragment '".concat(fragment.name, "'."), errorContext.map(function (node) {
return node.loc;
}));
}
fragments.set(fragmentName, null); // to detect circular references
var transformedFragment = null;
var selections = transformSelections(context, fragments, fragmentScope, fragment.selections, errorContext);
if (selections) {
transformedFragment = (0, _objectSpread2["default"])({}, fragment, {
selections: selections,
name: fragmentName,
argumentDefinitions: []
});
}
fragments.set(fragmentName, transformedFragment);
return transformedFragment;
}
function hashArguments(args, scope, errorContext) {
if (!args.length) {
return null;
}
var sortedArgs = (0, _toConsumableArray2["default"])(args).sort(function (a, b) {
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
});
var printedArgs = JSON.stringify(sortedArgs.map(function (arg) {
var value;
if (arg.value.kind === 'Variable') {
value = scope[arg.value.variableName];
if (value == null) {
// This transform does whole-program optimization, errors in
// a single document could break invariants and/or cause
// additional spurious errors.
throw createNonRecoverableUserError("Variable '$".concat(arg.value.variableName, "' is not in scope."), [arg.value.loc]);
}
} else {
value = arg.value;
}
return {
name: arg.name,
value: getIdentifierForArgumentValue(value)
};
}));
return murmurHash(printedArgs);
}
module.exports = {
transform: relayApplyFragmentArgumentTransform
};