@ardatan/relay-compiler
Version:
A compiler tool for building GraphQL-driven applications.
1,101 lines (1,034 loc) • 31.9 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 FlattenTransform = require('../../transforms/FlattenTransform');
const IRVisitor = require('../../core/IRVisitor');
const MaskTransform = require('../../transforms/MaskTransform');
const MatchTransform = require('../../transforms/MatchTransform');
const Profiler = require('../../core/GraphQLCompilerProfiler');
const RefetchableFragmentTransform = require('../../transforms/RefetchableFragmentTransform');
const RelayDirectiveTransform = require('../../transforms/RelayDirectiveTransform');
const RequiredFieldTransform = require('../../transforms/RequiredFieldTransform');
const generateAbstractTypeRefinementKey = require('../../util/generateAbstractTypeRefinementKey');
const partitionArray = require('../../util/partitionArray');
const {
anyTypeAlias,
declareExportOpaqueType,
exactObjectTypeAnnotation,
exportType,
exportTypes,
importTypes,
inexactObjectTypeAnnotation,
intersectionTypeAnnotation,
lineComments,
readOnlyArrayOfType,
readOnlyObjectTypeProperty,
unionTypeAnnotation,
} = require('./RelayFlowBabelFactories');
const {
transformInputType,
transformScalarType,
} = require('./RelayFlowTypeTransformers');
import type {IRTransform} from '../../core/CompilerContext';
import type {
Fragment,
Root,
Directive,
Metadata,
ModuleImport,
Selection as IRSelection,
} from '../../core/IR';
import type {NodeVisitor} from '../../core/IRVisitor';
import type {Schema, TypeID, EnumTypeID} from '../../core/Schema';
import type {RequiredDirectiveMetadata} from '../../transforms/RequiredFieldTransform';
import type {TypeGeneratorOptions} from '../RelayLanguagePluginInterface';
const babelGenerator = require('@babel/generator').default;
const t = require('@babel/types');
const invariant = require('invariant');
const nullthrows = require('nullthrows');
export type State = {|
...TypeGeneratorOptions,
+generatedFragments: Set<string>,
+generatedInputObjectTypes: {[name: string]: TypeID | 'pending', ...},
+usedEnums: {[name: string]: EnumTypeID, ...},
+usedFragments: Set<string>,
+matchFields: Map<string, mixed>,
+runtimeImports: Set<string>,
|};
function generate(
schema: Schema,
node: Root | Fragment,
options: TypeGeneratorOptions,
): string {
const ast = IRVisitor.visit(node, createVisitor(schema, options));
return babelGenerator(ast).code;
}
type Selection = {|
+key: string,
+schemaName?: string,
+value?: any,
+nodeType?: TypeID,
+conditional?: boolean,
+concreteType?: string,
+ref?: string,
+nodeSelections?: ?SelectionMap,
+kind?: string,
+documentName?: string,
|};
type SelectionMap = Map<string, Selection>;
function makeProp(
schema: Schema,
{key, schemaName, value, conditional, nodeType, nodeSelections}: Selection,
state: State,
unmasked: boolean,
concreteType?: string,
) {
if (schemaName === '__typename' && concreteType) {
value = t.stringLiteralTypeAnnotation(concreteType);
} else if (nodeType) {
value = transformScalarType(
schema,
nodeType,
state,
selectionsToBabel(
schema,
[Array.from(nullthrows(nodeSelections).values())],
state,
unmasked,
),
);
}
const typeProperty = readOnlyObjectTypeProperty(key, value);
if (conditional) {
typeProperty.optional = true;
}
return typeProperty;
}
const isTypenameSelection = selection => selection.schemaName === '__typename';
const hasTypenameSelection = selections => selections.some(isTypenameSelection);
const onlySelectsTypename = selections => selections.every(isTypenameSelection);
function selectionsToBabel(
schema: Schema,
selections: $ReadOnlyArray<$ReadOnlyArray<Selection>>,
state: State,
unmasked: boolean,
fragmentTypeName?: string,
) {
const baseFields = new Map();
const byConcreteType = {};
flattenArray(selections).forEach(selection => {
const {concreteType} = selection;
if (concreteType) {
byConcreteType[concreteType] = byConcreteType[concreteType] ?? [];
byConcreteType[concreteType].push(selection);
} else {
const previousSel = baseFields.get(selection.key);
baseFields.set(
selection.key,
previousSel ? mergeSelection(selection, previousSel) : selection,
);
}
});
const types = [];
if (
Object.keys(byConcreteType).length > 0 &&
onlySelectsTypename(Array.from(baseFields.values())) &&
(hasTypenameSelection(Array.from(baseFields.values())) ||
Object.keys(byConcreteType).every(type =>
hasTypenameSelection(byConcreteType[type]),
))
) {
const typenameAliases = new Set();
for (const concreteType in byConcreteType) {
types.push(
groupRefs([
...Array.from(baseFields.values()),
...byConcreteType[concreteType],
]).map(selection => {
if (selection.schemaName === '__typename') {
typenameAliases.add(selection.key);
}
return makeProp(schema, selection, state, unmasked, concreteType);
}),
);
}
// It might be some other type then the listed concrete types. Ideally, we
// would set the type to diff(string, set of listed concrete types), but
// this doesn't exist in Flow at the time.
types.push(
Array.from(typenameAliases).map(typenameAlias => {
const otherProp = readOnlyObjectTypeProperty(
typenameAlias,
t.stringLiteralTypeAnnotation('%other'),
);
otherProp.leadingComments = lineComments(
"This will never be '%other', but we need some",
'value in case none of the concrete values match.',
);
return otherProp;
}),
);
} else {
let selectionMap = selectionsToMap(Array.from(baseFields.values()));
for (const concreteType in byConcreteType) {
selectionMap = mergeSelections(
selectionMap,
selectionsToMap(
byConcreteType[concreteType].map(sel => ({
...sel,
conditional: true,
})),
),
);
}
const selectionMapValues = groupRefs(
Array.from(selectionMap.values()),
).map(sel =>
isTypenameSelection(sel) && sel.concreteType
? makeProp(
schema,
{...sel, conditional: false},
state,
unmasked,
sel.concreteType,
)
: makeProp(schema, sel, state, unmasked),
);
types.push(selectionMapValues);
}
return unionTypeAnnotation(
types.map(props => {
if (fragmentTypeName) {
props.push(
readOnlyObjectTypeProperty(
'$refType',
t.genericTypeAnnotation(t.identifier(fragmentTypeName)),
),
);
}
return unmasked
? inexactObjectTypeAnnotation(props)
: exactObjectTypeAnnotation(props);
}),
);
}
function mergeSelection(
a: ?Selection,
b: Selection,
shouldSetConditional: boolean = true,
): Selection {
if (!a) {
if (shouldSetConditional) {
return {
...b,
conditional: true,
};
}
return b;
}
return {
...a,
nodeSelections: a.nodeSelections
? mergeSelections(
a.nodeSelections,
nullthrows(b.nodeSelections),
shouldSetConditional,
)
: null,
conditional: a.conditional && b.conditional,
};
}
function mergeSelections(
a: SelectionMap,
b: SelectionMap,
shouldSetConditional: boolean = true,
): SelectionMap {
const merged = new Map();
for (const [key, value] of a.entries()) {
merged.set(key, value);
}
for (const [key, value] of b.entries()) {
merged.set(key, mergeSelection(a.get(key), value, shouldSetConditional));
}
return merged;
}
function isPlural(node: Fragment): boolean {
return Boolean(node.metadata && node.metadata.plural);
}
function createVisitor(
schema: Schema,
options: TypeGeneratorOptions,
): NodeVisitor {
const state = {
customScalars: options.customScalars,
enumsHasteModule: options.enumsHasteModule,
generatedFragments: new Set(),
generatedInputObjectTypes: {},
optionalInputFields: options.optionalInputFields,
usedEnums: {},
usedFragments: new Set(),
useHaste: options.useHaste,
useSingleArtifactDirectory: options.useSingleArtifactDirectory,
noFutureProofEnums: options.noFutureProofEnums,
matchFields: new Map(),
runtimeImports: new Set(),
};
return {
leave: {
Root(node: Root) {
const inputVariablesType = generateInputVariablesType(
schema,
node,
state,
);
const inputObjectTypes = generateInputObjectTypes(state);
let responseTypeDefinition = selectionsToBabel(
schema,
/* $FlowFixMe: selections have already been transformed */
(node.selections: $ReadOnlyArray<$ReadOnlyArray<Selection>>),
state,
false,
);
if (node.metadata?.childrenCanBubbleNull === true) {
responseTypeDefinition = t.nullableTypeAnnotation(
responseTypeDefinition,
);
}
const responseType = exportType(
`${node.name}Response`,
responseTypeDefinition,
);
const operationTypes = [
t.objectTypeProperty(
t.identifier('variables'),
t.genericTypeAnnotation(t.identifier(`${node.name}Variables`)),
),
t.objectTypeProperty(
t.identifier('response'),
t.genericTypeAnnotation(t.identifier(`${node.name}Response`)),
),
];
// Generate raw response type
let rawResponseType;
const {normalizationIR} = options;
if (
normalizationIR &&
node.directives.some(d => d.name === DIRECTIVE_NAME)
) {
rawResponseType = IRVisitor.visit(
normalizationIR,
createRawResponseTypeVisitor(schema, state),
);
}
const refetchableFragmentName = getRefetchableQueryParentFragmentName(
state,
node.metadata,
);
if (refetchableFragmentName != null) {
state.runtimeImports.add('FragmentReference');
}
const babelNodes = [];
if (state.runtimeImports.size) {
babelNodes.push(
importTypes(
Array.from(state.runtimeImports).sort(),
'relay-runtime',
),
);
}
babelNodes.push(
...(refetchableFragmentName
? generateFragmentRefsForRefetchable(refetchableFragmentName)
: getFragmentImports(state)),
...getEnumDefinitions(schema, state),
...inputObjectTypes,
inputVariablesType,
responseType,
);
if (rawResponseType) {
for (const [key, ast] of state.matchFields) {
babelNodes.push(exportType(key, ast));
}
operationTypes.push(
t.objectTypeProperty(
t.identifier('rawResponse'),
t.genericTypeAnnotation(t.identifier(`${node.name}RawResponse`)),
),
);
babelNodes.push(rawResponseType);
}
babelNodes.push(
exportType(node.name, exactObjectTypeAnnotation(operationTypes)),
);
// $FlowFixMe[incompatible-call]
return t.program(babelNodes);
},
Fragment(node) {
let selections = flattenArray(
// $FlowFixMe[incompatible-cast] : selections have already been transformed
(node.selections: $ReadOnlyArray<$ReadOnlyArray<Selection>>),
);
const numConecreteSelections = selections.filter(s => s.concreteType)
.length;
selections = selections.map(selection => {
if (
numConecreteSelections <= 1 &&
isTypenameSelection(selection) &&
!schema.isAbstractType(node.type)
) {
return [
{
...selection,
concreteType: schema.getTypeString(node.type),
},
];
}
return [selection];
});
state.generatedFragments.add(node.name);
const fragmentTypes = getFragmentTypes(
node.name,
getRefetchableQueryPath(state, node.directives),
);
const refTypeName = getRefTypeName(node.name);
const refTypeDataProperty = readOnlyObjectTypeProperty(
'$data',
t.genericTypeAnnotation(t.identifier(`${node.name}$data`)),
);
refTypeDataProperty.optional = true;
const refTypeFragmentRefProperty = readOnlyObjectTypeProperty(
'$fragmentRefs',
t.genericTypeAnnotation(
t.identifier(getOldFragmentTypeName(node.name)),
),
);
const isPluralFragment = isPlural(node);
const refType = inexactObjectTypeAnnotation([
refTypeDataProperty,
refTypeFragmentRefProperty,
]);
const dataTypeName = getDataTypeName(node.name);
const dataType = t.genericTypeAnnotation(t.identifier(node.name));
const unmasked = node.metadata != null && node.metadata.mask === false;
const baseType = selectionsToBabel(
schema,
selections,
state,
unmasked,
unmasked ? undefined : getOldFragmentTypeName(node.name),
);
let type = isPluralFragment ? readOnlyArrayOfType(baseType) : baseType;
if (node.metadata?.childrenCanBubbleNull === true) {
type = t.nullableTypeAnnotation(type);
}
state.runtimeImports.add('FragmentReference');
return t.program([
...getFragmentImports(state),
...getEnumDefinitions(schema, state),
importTypes(Array.from(state.runtimeImports).sort(), 'relay-runtime'),
...fragmentTypes,
exportType(node.name, type),
exportType(dataTypeName, dataType),
exportType(
refTypeName,
isPluralFragment ? readOnlyArrayOfType(refType) : refType,
),
]);
},
InlineFragment(node) {
return flattenArray(
// $FlowFixMe[incompatible-cast] : selections have already been transformed
(node.selections: $ReadOnlyArray<$ReadOnlyArray<Selection>>),
).map(typeSelection => {
return schema.isAbstractType(node.typeCondition)
? {
...typeSelection,
conditional: true,
}
: {
...typeSelection,
concreteType: schema.getTypeString(node.typeCondition),
};
});
},
Condition(node) {
return flattenArray(
// $FlowFixMe[incompatible-cast] : selections have already been transformed
(node.selections: $ReadOnlyArray<$ReadOnlyArray<Selection>>),
).map(selection => {
return {
...selection,
conditional: true,
};
});
},
ScalarField(node) {
return visitScalarField(schema, node, state);
},
LinkedField(node) {
return visitLinkedField(schema, node);
},
ModuleImport(node) {
return [
{
key: '__fragmentPropName',
conditional: true,
value: transformScalarType(
schema,
schema.expectStringType(),
state,
),
},
{
key: '__module_component',
conditional: true,
value: transformScalarType(
schema,
schema.expectStringType(),
state,
),
},
{
key: '__fragments_' + node.name,
ref: node.name,
},
];
},
FragmentSpread(node) {
state.usedFragments.add(node.name);
return [
{
key: '__fragments_' + node.name,
ref: node.name,
},
];
},
},
};
}
function visitNodeWithSelectionsOnly(node) {
return flattenArray(
// $FlowFixMe[incompatible-cast] : selections have already been transformed
(node.selections: $ReadOnlyArray<$ReadOnlyArray<Selection>>),
);
}
function visitScalarField(schema: Schema, node, state: State) {
const requiredMetadata: ?RequiredDirectiveMetadata = (node.metadata
?.required: $FlowFixMe);
const nodeType =
requiredMetadata != null ? schema.getNonNullType(node.type) : node.type;
return [
{
key: node.alias,
schemaName: node.name,
value: transformScalarType(schema, nodeType, state),
},
];
}
function getLinkedFieldNodeType(schema: Schema, node) {
const requiredMetadata: ?RequiredDirectiveMetadata = (node.metadata
?.required: $FlowFixMe);
if (requiredMetadata != null) {
return schema.getNonNullType(node.type);
}
if (node.metadata?.childrenCanBubbleNull === true) {
if (schema.isList(node.type)) {
// In a plural field, nulls bubble up to the item, resulting in a list of nullable items.
return schema.mapListItemType(node.type, inner =>
schema.getNullableType(inner),
);
} else if (schema.isNonNull(node.type)) {
const nullable = schema.getNullableType(node.type);
if (schema.isList(nullable)) {
return schema.getNonNullType(
schema.mapListItemType(nullable, inner =>
schema.getNullableType(inner),
),
);
}
return nullable;
}
return node.type;
}
return node.type;
}
function visitLinkedField(schema: Schema, node) {
return [
{
key: node.alias,
schemaName: node.name,
nodeType: getLinkedFieldNodeType(schema, node),
nodeSelections: selectionsToMap(
flattenArray(
// $FlowFixMe[incompatible-cast] : selections have already been transformed
(node.selections: $ReadOnlyArray<$ReadOnlyArray<Selection>>),
),
/*
* append concreteType to key so overlapping fields with different
* concreteTypes don't get overwritten by each other
*/
true,
),
},
];
}
function makeRawResponseProp(
schema: Schema,
{
key,
schemaName,
value,
conditional,
nodeType,
nodeSelections,
kind,
}: Selection,
state: State,
concreteType: ?string,
) {
if (kind === 'ModuleImport') {
return t.objectTypeSpreadProperty(
t.genericTypeAnnotation(t.identifier(key)),
);
}
if (schemaName === '__typename' && concreteType) {
value = t.stringLiteralTypeAnnotation(concreteType);
} else if (nodeType) {
value = transformScalarType(
schema,
nodeType,
state,
selectionsToRawResponseBabel(
schema,
[Array.from(nullthrows(nodeSelections).values())],
state,
schema.isAbstractType(nodeType) || schema.isWrapper(nodeType)
? null
: schema.getTypeString(nodeType),
),
);
}
const typeProperty = readOnlyObjectTypeProperty(key, value);
if (conditional) {
typeProperty.optional = true;
}
return typeProperty;
}
// Trasform the codegen IR selections into Babel flow types
function selectionsToRawResponseBabel(
schema: Schema,
selections: $ReadOnlyArray<$ReadOnlyArray<Selection>>,
state: State,
nodeTypeName: ?string,
) {
const baseFields = [];
const byConcreteType = {};
flattenArray(selections).forEach(selection => {
const {concreteType} = selection;
if (concreteType) {
byConcreteType[concreteType] = byConcreteType[concreteType] ?? [];
byConcreteType[concreteType].push(selection);
} else {
baseFields.push(selection);
}
});
const types = [];
if (Object.keys(byConcreteType).length) {
const baseFieldsMap = selectionsToMap(baseFields);
for (const concreteType in byConcreteType) {
const mergedSeletions = Array.from(
mergeSelections(
baseFieldsMap,
selectionsToMap(byConcreteType[concreteType]),
false,
).values(),
);
types.push(
exactObjectTypeAnnotation(
mergedSeletions.map(selection =>
makeRawResponseProp(schema, selection, state, concreteType),
),
),
);
appendLocal3DPayload(types, mergedSeletions, schema, state, concreteType);
}
}
if (baseFields.length > 0) {
types.push(
exactObjectTypeAnnotation(
baseFields.map(selection =>
makeRawResponseProp(schema, selection, state, nodeTypeName),
),
),
);
appendLocal3DPayload(types, baseFields, schema, state, nodeTypeName);
}
return unionTypeAnnotation(types);
}
function appendLocal3DPayload(
types: Array<mixed>,
selections: $ReadOnlyArray<Selection>,
schema: Schema,
state: State,
currentType: ?string,
): void {
const moduleImport = selections.find(sel => sel.kind === 'ModuleImport');
if (moduleImport) {
// Generate an extra opaque type for client 3D fields
state.runtimeImports.add('Local3DPayload');
types.push(
t.genericTypeAnnotation(
t.identifier('Local3DPayload'),
t.typeParameterInstantiation([
t.stringLiteralTypeAnnotation(nullthrows(moduleImport.documentName)),
exactObjectTypeAnnotation(
selections
.filter(sel => sel.schemaName !== 'js')
.map(selection =>
makeRawResponseProp(schema, selection, state, currentType),
),
),
]),
),
);
}
}
// Visitor for generating raw response type
function createRawResponseTypeVisitor(schema: Schema, state: State) {
return {
leave: {
Root(node) {
return exportType(
`${node.name}RawResponse`,
selectionsToRawResponseBabel(
schema,
// $FlowFixMe[incompatible-cast] : selections have already been transformed
(node.selections: $ReadOnlyArray<$ReadOnlyArray<Selection>>),
state,
null,
),
);
},
InlineFragment(node) {
const typeCondition = node.typeCondition;
return flattenArray(
// $FlowFixMe[incompatible-cast] : selections have already been transformed
(node.selections: $ReadOnlyArray<$ReadOnlyArray<Selection>>),
).map(typeSelection => {
return schema.isAbstractType(typeCondition)
? typeSelection
: {
...typeSelection,
concreteType: schema.getTypeString(typeCondition),
};
});
},
ScalarField(node) {
return visitScalarField(schema, node, state);
},
ClientExtension(node) {
return flattenArray(
// $FlowFixMe[incompatible-cast] : selections have already been transformed
(node.selections: $ReadOnlyArray<$ReadOnlyArray<Selection>>),
).map(sel => ({
...sel,
conditional: true,
}));
},
LinkedField(node) {
return visitLinkedField(schema, node);
},
Condition: visitNodeWithSelectionsOnly,
Defer: visitNodeWithSelectionsOnly,
Stream: visitNodeWithSelectionsOnly,
ModuleImport(node) {
return visitRawResposneModuleImport(schema, node, state);
},
FragmentSpread(node) {
invariant(
false,
'A fragment spread is found when traversing the AST, ' +
'make sure you are passing the codegen IR',
);
},
},
};
}
// Dedupe the generated type of module selections to reduce file size
function visitRawResposneModuleImport(
schema: Schema,
node: ModuleImport,
state: State,
): $ReadOnlyArray<Selection> {
const {selections, name: key} = node;
const moduleSelections = selections
.filter(
// $FlowFixMe[prop-missing] selections have already been transformed
sel => sel.length && sel[0].schemaName === 'js',
)
.map(arr => arr[0]);
if (!state.matchFields.has(key)) {
const ast = selectionsToRawResponseBabel(
schema,
// $FlowFixMe[incompatible-cast] : selections have already been transformed
(node.selections: $ReadOnlyArray<$ReadOnlyArray<Selection>>).filter(
sel => sel.length > 1 || sel[0].schemaName !== 'js',
),
state,
null,
);
state.matchFields.set(key, ast);
}
return [
...moduleSelections,
{
key,
kind: 'ModuleImport',
documentName: node.key,
},
];
}
function selectionsToMap(
selections: $ReadOnlyArray<Selection>,
appendType?: boolean,
): SelectionMap {
const map = new Map();
selections.forEach(selection => {
const key =
appendType && selection.concreteType
? `${selection.key}::${selection.concreteType}`
: selection.key;
const previousSel = map.get(key);
map.set(
key,
previousSel ? mergeSelection(previousSel, selection) : selection,
);
});
return map;
}
function flattenArray(
arrayOfArrays: $ReadOnlyArray<$ReadOnlyArray<Selection>>,
): $ReadOnlyArray<Selection> {
const result = [];
arrayOfArrays.forEach(array => {
result.push(...array);
});
return result;
}
function generateInputObjectTypes(state: State) {
return Object.keys(state.generatedInputObjectTypes).map(typeIdentifier => {
const inputObjectType = state.generatedInputObjectTypes[typeIdentifier];
invariant(
typeof inputObjectType !== 'string',
'RelayCompilerFlowGenerator: Expected input object type to have been' +
' defined before calling `generateInputObjectTypes`',
);
return exportType(typeIdentifier, inputObjectType);
});
}
function generateInputVariablesType(schema: Schema, node: Root, state: State) {
return exportType(
`${node.name}Variables`,
exactObjectTypeAnnotation(
node.argumentDefinitions.map(arg => {
const property = t.objectTypeProperty(
t.identifier(arg.name),
transformInputType(schema, arg.type, state),
);
if (!schema.isNonNull(arg.type)) {
property.optional = true;
}
return property;
}),
),
);
}
function groupRefs(props): $ReadOnlyArray<Selection> {
const result = [];
const refs = [];
props.forEach(prop => {
if (prop.ref) {
refs.push(prop.ref);
} else {
result.push(prop);
}
});
if (refs.length > 0) {
const value = intersectionTypeAnnotation(
refs.map(ref =>
t.genericTypeAnnotation(t.identifier(getOldFragmentTypeName(ref))),
),
);
result.push({
key: '$fragmentRefs',
conditional: false,
value,
});
}
return result;
}
function getFragmentImports(state: State) {
const imports = [];
if (state.usedFragments.size > 0) {
const usedFragments = Array.from(state.usedFragments).sort();
for (const usedFragment of usedFragments) {
const fragmentTypeName = getOldFragmentTypeName(usedFragment);
if (!state.generatedFragments.has(usedFragment)) {
if (state.useHaste) {
// TODO(T22653277) support non-haste environments when importing
// fragments
imports.push(
importTypes([fragmentTypeName], usedFragment + '.graphql'),
);
} else if (state.useSingleArtifactDirectory) {
imports.push(
importTypes([fragmentTypeName], './' + usedFragment + '.graphql'),
);
} else {
imports.push(anyTypeAlias(fragmentTypeName));
}
}
}
}
return imports;
}
function getEnumDefinitions(
schema: Schema,
{enumsHasteModule, usedEnums, noFutureProofEnums}: State,
) {
const enumNames = Object.keys(usedEnums).sort();
if (enumNames.length === 0) {
return [];
}
if (typeof enumsHasteModule === 'string') {
return [importTypes(enumNames, enumsHasteModule)];
}
if (typeof enumsHasteModule === 'function') {
return enumNames.map(enumName =>
importTypes([enumName], enumsHasteModule(enumName)),
);
}
return enumNames.map(name => {
const values = [].concat(schema.getEnumValues(usedEnums[name]));
values.sort();
if (!noFutureProofEnums) {
values.push('%future added value');
}
return exportType(
name,
t.unionTypeAnnotation(
values.map(value => t.stringLiteralTypeAnnotation(value)),
),
);
});
}
// If it's a @refetchable fragment, we generate the $fragmentRef in generated
// query, and import it in the fragment to avoid circular dependencies
function getRefetchableQueryParentFragmentName(
state: State,
metadata: Metadata,
): ?string {
if (
!metadata?.isRefetchableQuery ||
(!state.useHaste && !state.useSingleArtifactDirectory)
) {
return null;
}
const derivedFrom = metadata?.derivedFrom;
if (derivedFrom != null && typeof derivedFrom === 'string') {
return derivedFrom;
}
return null;
}
function getRefetchableQueryPath(
state: State,
directives: $ReadOnlyArray<Directive>,
): ?string {
let refetchableQuery: ?string;
if (!state.useHaste && !state.useSingleArtifactDirectory) {
return;
}
const refetchableArgs = directives.find(d => d.name === 'refetchable')?.args;
if (!refetchableArgs) {
return;
}
const argument = refetchableArgs.find(
arg => arg.kind === 'Argument' && arg.name === 'queryName',
);
if (
argument &&
argument.value &&
argument.value.kind === 'Literal' &&
typeof argument.value.value === 'string'
) {
refetchableQuery = argument.value.value;
if (!state.useHaste) {
refetchableQuery = './' + refetchableQuery;
}
refetchableQuery += '.graphql';
}
return refetchableQuery;
}
function generateFragmentRefsForRefetchable(name: string) {
const oldFragmentTypeName = getOldFragmentTypeName(name);
const newFragmentTypeName = getNewFragmentTypeName(name);
return [
declareExportOpaqueType(oldFragmentTypeName, 'FragmentReference'),
declareExportOpaqueType(newFragmentTypeName, oldFragmentTypeName),
];
}
function getFragmentTypes(name: string, refetchableQueryPath: ?string) {
const oldFragmentTypeName = getOldFragmentTypeName(name);
const newFragmentTypeName = getNewFragmentTypeName(name);
if (refetchableQueryPath) {
return [
importTypes(
[oldFragmentTypeName, newFragmentTypeName],
refetchableQueryPath,
),
exportTypes([oldFragmentTypeName, newFragmentTypeName]),
];
}
return [
declareExportOpaqueType(oldFragmentTypeName, 'FragmentReference'),
declareExportOpaqueType(newFragmentTypeName, oldFragmentTypeName),
];
}
function getOldFragmentTypeName(name: string) {
return `${name}$ref`;
}
function getNewFragmentTypeName(name: string) {
return `${name}$fragmentType`;
}
function getRefTypeName(name: string): string {
return `${name}$key`;
}
function getDataTypeName(name: string): string {
return `${name}$data`;
}
const FLOW_TRANSFORMS: $ReadOnlyArray<IRTransform> = [
RelayDirectiveTransform.transform,
MaskTransform.transform,
MatchTransform.transform,
RequiredFieldTransform.transform,
FlattenTransform.transformWithOptions({}),
RefetchableFragmentTransform.transform,
];
const DIRECTIVE_NAME = 'raw_response_type';
module.exports = {
generate: (Profiler.instrument(generate, 'RelayFlowGenerator.generate'): (
schema: Schema,
node: Root | Fragment,
options: TypeGeneratorOptions,
) => string),
transforms: FLOW_TRANSFORMS,
SCHEMA_EXTENSION: `directive @${DIRECTIVE_NAME} on QUERY | MUTATION | SUBSCRIPTION`,
};