UNPKG

@ardatan/relay-compiler

Version:

A compiler tool for building GraphQL-driven applications.

393 lines (323 loc) 14.1 kB
/** * 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 */ // flowlint ambiguous-object-type:error 'use strict'; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread2")); var _createForOfIteratorHelper2 = _interopRequireDefault(require("@babel/runtime/helpers/createForOfIteratorHelper")); var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray")); var IRTransformer = require('../core/IRTransformer'); var RelayCompilerScope = require('../core/RelayCompilerScope'); var getIdentifierForArgumentValue = require('../core/getIdentifierForArgumentValue'); var murmurHash = require('../util/murmurHash'); var _require = require('../core/CompilerError'), createCompilerError = _require.createCompilerError, createNonRecoverableUserError = _require.createNonRecoverableUserError; var getFragmentScope = RelayCompilerScope.getFragmentScope, getRootScope = RelayCompilerScope.getRootScope; /** * A transform 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 applyFragmentArgumentTransform(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]); }, SplitOperation: function SplitOperation(node) { return transformNode(context, fragments, {}, node, [node]); }, // Fragments are included below where referenced. // Unreferenced fragments are not included. Fragment: function Fragment() { return null; } }); var _iterator = (0, _createForOfIteratorHelper2["default"])(fragments.values()), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var pendingFragment = _step.value; if (pendingFragment.kind === 'resolved' && pendingFragment.value) { nextContext = nextContext.add(pendingFragment.value); } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } return 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); return (0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, node), {}, { directives: directives, selections: selections }); } return (0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, node), {}, { selections: selections }); } function transformDeferStreamNode(context, fragments, scope, node, errorContext) { var nextNode = transformNode(context, fragments, scope, node, errorContext); if (!nextNode) { return null; } nextNode; if (nextNode["if"]) { var ifVal = transformValue(scope, nextNode["if"], errorContext); if (ifVal.kind === 'Literal' && ifVal.value === false && node.selections && node.selections.length === 1) { // Skip Defer/Stream wrapper with literal if: false return node.selections[0]; } // $FlowFixMe[cannot-write] nextNode is uniquely owned nextNode["if"] = ifVal; } if (nextNode.useCustomizedBatch) { // $FlowFixMe[cannot-write] nextNode is uniquely owned nextNode.useCustomizedBatch = transformValue(scope, nextNode.useCustomizedBatch, errorContext); } if (nextNode.initialCount) { // $FlowFixMe[cannot-write] nextNode is uniquely owned nextNode.initialCount = transformValue(scope, nextNode.initialCount, errorContext); } return nextNode; } function transformFragmentSpread(context, fragments, scope, spread, errorContext) { var directives = transformDirectives(scope, spread.directives, errorContext); var appliedFragment = transformFragment(context, fragments, scope, spread, spread.args, [].concat((0, _toConsumableArray2["default"])(errorContext), [spread])); if (!appliedFragment) { return null; } var transformed = (0, _objectSpread2["default"])((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') { var selections = transformSelections(context, fragments, scope, field.selections, errorContext); if (!selections) { return null; } return (0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, field), {}, { args: args, directives: directives, selections: selections }); } else { return (0, _objectSpread2["default"])((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"])((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 === 'ClientExtension' || selection.kind === 'InlineDataFragmentSpread' || selection.kind === 'InlineFragment' || selection.kind === 'ModuleImport') { nextSelection = transformNode(context, fragments, scope, selection, errorContext); } else if (selection.kind === 'Defer' || selection.kind === 'Stream') { nextSelection = transformDeferStreamNode(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') { nextSelection = transformField(context, fragments, scope, selection, errorContext); } else { selection; throw createCompilerError("ApplyFragmentArgumentTransform: 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"])((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"])((0, _objectSpread2["default"])({}, arg), {}, { value: value }); }); } function transformValue(scope, value, errorContext) { if (value.kind === 'Variable') { var scopeValue = scope[value.variableName]; if (scopeValue == null) { var _errorContext$; // 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."), [(_errorContext$ = errorContext[0]) === null || _errorContext$ === void 0 ? void 0 : _errorContext$.loc, value.loc].filter(Boolean)); } return scopeValue; } else if (value.kind === 'ObjectValue') { return (0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, value), {}, { fields: value.fields.map(function (field) { return (0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, field), {}, { value: transformValue(scope, field.value, errorContext) }); }) }); } else if (value.kind === 'ListValue') { return (0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, value), {}, { items: value.items.map(function (item) { return transformValue(scope, item, 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 schema = context.getSchema(); var fragment = context.getFragment(spread.name, spread.loc); var argumentsHash = hashArguments(args, parentScope, errorContext); var fragmentName = argumentsHash ? "".concat(fragment.name, "_").concat(argumentsHash) : fragment.name; var appliedFragment = fragments.get(fragmentName); if (appliedFragment) { if (appliedFragment.kind === 'resolved') { return appliedFragment.value; } else { // 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; })); } } var fragmentScope = getFragmentScope(schema, fragment.argumentDefinitions, args, parentScope, spread); // record that this fragment is pending to detect circular references fragments.set(fragmentName, { kind: 'pending' }); var transformedFragment = null; var selections = transformSelections(context, fragments, fragmentScope, fragment.selections, errorContext); if (selections) { transformedFragment = (0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, fragment), {}, { selections: selections, name: fragmentName, argumentDefinitions: [] }); } fragments.set(fragmentName, { kind: 'resolved', value: 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) { var _errorContext$2; // 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."), [(_errorContext$2 = errorContext[0]) === null || _errorContext$2 === void 0 ? void 0 : _errorContext$2.loc, arg.value.loc].filter(Boolean)); } } else { value = arg.value; } return { name: arg.name, value: getIdentifierForArgumentValue(value) }; })); return murmurHash(printedArgs); } module.exports = { transform: applyFragmentArgumentTransform };