@ardatan/relay-compiler
Version:
A compiler tool for building GraphQL-driven applications.
527 lines (504 loc) • 14.2 kB
Flow
/**
* 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.
*
* @flow
* @format
*/
// flowlint ambiguous-object-type:error
;
const IRTransformer = require('../core/IRTransformer');
const RelayCompilerScope = require('../core/RelayCompilerScope');
const getIdentifierForArgumentValue = require('../core/getIdentifierForArgumentValue');
const murmurHash = require('../util/murmurHash');
const {
createCompilerError,
createNonRecoverableUserError,
} = require('../core/CompilerError');
import type CompilerContext from '../core/CompilerContext';
import type {
Argument,
ArgumentValue,
Condition,
Defer,
Directive,
Field,
Fragment,
FragmentSpread,
IR,
Node,
Selection,
Stream,
} from '../core/IR';
import type {Scope} from '../core/RelayCompilerScope';
const {getFragmentScope, getRootScope} = RelayCompilerScope;
type PendingFragment =
| {|kind: 'pending'|}
| {|kind: 'resolved', value: ?Fragment|};
/**
* 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: CompilerContext,
): CompilerContext {
const fragments: Map<string, PendingFragment> = new Map();
let nextContext = IRTransformer.transform(context, {
Root: node => {
const scope = getRootScope(node.argumentDefinitions);
return transformNode(context, fragments, scope, node, [node]);
},
SplitOperation: node => {
return transformNode(context, fragments, {}, node, [node]);
},
// Fragments are included below where referenced.
// Unreferenced fragments are not included.
Fragment: () => null,
});
for (const pendingFragment of fragments.values()) {
if (pendingFragment.kind === 'resolved' && pendingFragment.value) {
nextContext = nextContext.add(pendingFragment.value);
}
}
return nextContext;
}
function transformNode<T: Node>(
context: CompilerContext,
fragments: Map<string, PendingFragment>,
scope: Scope,
node: T,
errorContext: $ReadOnlyArray<IR>,
): ?T {
const selections = transformSelections(
context,
fragments,
scope,
node.selections,
errorContext,
);
if (!selections) {
return null;
}
if (node.hasOwnProperty('directives')) {
const directives = transformDirectives(
scope,
(node: $FlowIssue).directives,
errorContext,
);
return ({
...node,
directives,
selections,
}: any);
}
return ({
...node,
selections,
}: $FlowIssue);
}
function transformDeferStreamNode<T: Defer | Stream>(
context: CompilerContext,
fragments: Map<string, PendingFragment>,
scope: Scope,
node: T,
errorContext: $ReadOnlyArray<IR>,
): ?Selection {
const nextNode = transformNode(context, fragments, scope, node, errorContext);
if (!nextNode) {
return null;
}
(nextNode: T);
if (nextNode.if) {
const 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: CompilerContext,
fragments: Map<string, PendingFragment>,
scope: Scope,
spread: FragmentSpread,
errorContext: $ReadOnlyArray<IR>,
): ?FragmentSpread {
const directives = transformDirectives(
scope,
spread.directives,
errorContext,
);
const appliedFragment = transformFragment(
context,
fragments,
scope,
spread,
spread.args,
[...errorContext, spread],
);
if (!appliedFragment) {
return null;
}
const transformed: FragmentSpread = {
...spread,
kind: 'FragmentSpread',
args: [],
directives,
name: appliedFragment.name,
};
return transformed;
}
function transformField<T: Field>(
context: CompilerContext,
fragments: Map<string, PendingFragment>,
scope: Scope,
field: T,
errorContext: $ReadOnlyArray<IR>,
): ?T {
const args = transformArguments(scope, field.args, errorContext);
const directives = transformDirectives(scope, field.directives, errorContext);
if (field.kind === 'LinkedField') {
const selections = transformSelections(
context,
fragments,
scope,
field.selections,
errorContext,
);
if (!selections) {
return null;
}
return ({
...field,
args,
directives,
selections,
}: $FlowFixMe);
} else {
return {
...field,
args,
directives,
};
}
}
function transformCondition(
context: CompilerContext,
fragments: Map<string, PendingFragment>,
scope: Scope,
node: Condition,
errorContext: $ReadOnlyArray<IR>,
): ?$ReadOnlyArray<Selection> {
const 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;
}
const 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 [
{
...node,
condition,
selections,
},
];
}
function transformSelections(
context: CompilerContext,
fragments: Map<string, PendingFragment>,
scope: Scope,
selections: $ReadOnlyArray<Selection>,
errorContext: $ReadOnlyArray<IR>,
): ?$ReadOnlyArray<Selection> {
let nextSelections = null;
selections.forEach(selection => {
let 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') {
const conditionSelections = transformCondition(
context,
fragments,
scope,
selection,
errorContext,
);
if (conditionSelections) {
nextSelections = nextSelections || [];
nextSelections.push(...conditionSelections);
}
} else if (
selection.kind === 'LinkedField' ||
selection.kind === 'ScalarField'
) {
nextSelection = transformField(
context,
fragments,
scope,
selection,
errorContext,
);
} else {
(selection: empty);
throw createCompilerError(
`ApplyFragmentArgumentTransform: Unsupported kind '${selection.kind}'.`,
[selection.loc],
);
}
if (nextSelection) {
nextSelections = nextSelections || [];
nextSelections.push(nextSelection);
}
});
return nextSelections;
}
function transformDirectives(
scope: Scope,
directives: $ReadOnlyArray<Directive>,
errorContext: $ReadOnlyArray<IR>,
): $ReadOnlyArray<Directive> {
return directives.map(directive => {
const args = transformArguments(scope, directive.args, errorContext);
return {
...directive,
args,
};
});
}
function transformArguments(
scope: Scope,
args: $ReadOnlyArray<Argument>,
errorContext: $ReadOnlyArray<IR>,
): $ReadOnlyArray<Argument> {
return args.map(arg => {
const value = transformValue(scope, arg.value, errorContext);
return value === arg.value ? arg : {...arg, value};
});
}
function transformValue(
scope: Scope,
value: ArgumentValue,
errorContext: $ReadOnlyArray<IR>,
): ArgumentValue {
if (value.kind === 'Variable') {
const 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 '$${value.variableName}' is not in scope.`,
[errorContext[0]?.loc, value.loc].filter(Boolean),
);
}
return scopeValue;
} else if (value.kind === 'ObjectValue') {
return {
...value,
fields: value.fields.map(field => ({
...field,
value: transformValue(scope, field.value, errorContext),
})),
};
} else if (value.kind === 'ListValue') {
return {
...value,
items: value.items.map(item => 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: CompilerContext,
fragments: Map<string, PendingFragment>,
parentScope: Scope,
spread: FragmentSpread,
args: $ReadOnlyArray<Argument>,
errorContext: $ReadOnlyArray<IR>,
): ?Fragment {
const schema = context.getSchema();
const fragment = context.getFragment(spread.name, spread.loc);
const argumentsHash = hashArguments(args, parentScope, errorContext);
const fragmentName = argumentsHash
? `${fragment.name}_${argumentsHash}`
: fragment.name;
const 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 '${fragment.name}'.`,
errorContext.map(node => node.loc),
);
}
}
const fragmentScope = getFragmentScope(
schema,
fragment.argumentDefinitions,
args,
parentScope,
spread,
);
// record that this fragment is pending to detect circular references
fragments.set(fragmentName, {kind: 'pending'});
let transformedFragment = null;
const selections = transformSelections(
context,
fragments,
fragmentScope,
fragment.selections,
errorContext,
);
if (selections) {
transformedFragment = {
...fragment,
selections,
name: fragmentName,
argumentDefinitions: [],
};
}
fragments.set(fragmentName, {kind: 'resolved', value: transformedFragment});
return transformedFragment;
}
function hashArguments(
args: $ReadOnlyArray<Argument>,
scope: Scope,
errorContext: $ReadOnlyArray<IR>,
): ?string {
if (!args.length) {
return null;
}
const sortedArgs = [...args].sort((a, b) => {
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
});
const printedArgs = JSON.stringify(
sortedArgs.map(arg => {
let 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 '$${arg.value.variableName}' is not in scope.`,
[errorContext[0]?.loc, arg.value.loc].filter(Boolean),
);
}
} else {
value = arg.value;
}
return {
name: arg.name,
value: getIdentifierForArgumentValue(value),
};
}),
);
return murmurHash(printedArgs);
}
module.exports = {
transform: applyFragmentArgumentTransform,
};