Skip to content

Commit 54eb2e3

Browse files
authored
Split large unions over multiple lines (#10219)
* Split large unions over multiple lines * Add test for formatted output * Split more unions over multiple lines * Format members over multiple lines * Multiline more unions * Refactor * Lint * Always enable newlines in union of a certain size * Improve line wrapping * Ignore trailing commas * Ignore whitespace harder * Update snapshots
1 parent d72bbac commit 54eb2e3

File tree

9 files changed

+1224
-234
lines changed

9 files changed

+1224
-234
lines changed

packages/plugins/other/visitor-plugin-common/src/base-resolvers-visitor.ts

Lines changed: 46 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,54 +1105,58 @@ export class BaseResolversVisitor<
11051105
memberTypes: readonly GraphQLObjectType[] | GraphQLObjectType[];
11061106
isTypenameNonOptional: boolean;
11071107
}): string {
1108-
const result =
1109-
memberTypes
1110-
.map(type => {
1111-
const isTypeMapped = this.config.mappers[type.name];
1112-
// 1. If mapped without placehoder, just use it without doing extra checks
1113-
if (isTypeMapped && !hasPlaceholder(isTypeMapped.type)) {
1114-
return { typename: type.name, typeValue: isTypeMapped.type };
1115-
}
1108+
const members = memberTypes
1109+
.map(type => {
1110+
const isTypeMapped = this.config.mappers[type.name];
1111+
// 1. If mapped without placehoder, just use it without doing extra checks
1112+
if (isTypeMapped && !hasPlaceholder(isTypeMapped.type)) {
1113+
return { typename: type.name, typeValue: isTypeMapped.type };
1114+
}
11161115

1117-
// 2. Work out value for type
1118-
// 2a. By default, use the typescript type
1119-
let typeValue = this.convertName(type.name, {}, true);
1116+
// 2. Work out value for type
1117+
// 2a. By default, use the typescript type
1118+
let typeValue = this.convertName(type.name, {}, true);
11201119

1121-
// 2b. Find fields to Omit if needed.
1122-
// - If no field to Omit, "type with maybe Omit" is typescript type i.e. no Omit
1123-
// - If there are fields to Omit, keep track of these "type with maybe Omit" to replace in original unionMemberValue
1124-
const fieldsToOmit = this.getRelevantFieldsToOmit({
1125-
schemaType: type,
1126-
getTypeToUse: baseType => `_RefType['${baseType}']`,
1127-
});
1128-
if (fieldsToOmit.length > 0) {
1129-
typeValue = this.replaceFieldsInType(typeValue, fieldsToOmit);
1130-
}
1120+
// 2b. Find fields to Omit if needed.
1121+
// - If no field to Omit, "type with maybe Omit" is typescript type i.e. no Omit
1122+
// - If there are fields to Omit, keep track of these "type with maybe Omit" to replace in original unionMemberValue
1123+
const fieldsToOmit = this.getRelevantFieldsToOmit({
1124+
schemaType: type,
1125+
getTypeToUse: baseType => `_RefType['${baseType}']`,
1126+
});
1127+
if (fieldsToOmit.length > 0) {
1128+
typeValue = this.replaceFieldsInType(typeValue, fieldsToOmit);
1129+
}
11311130

1132-
// 2c. If type is mapped with placeholder, use the "type with maybe Omit" as {T}
1133-
if (isTypeMapped && hasPlaceholder(isTypeMapped.type)) {
1134-
return { typename: type.name, typeValue: replacePlaceholder(isTypeMapped.type, typeValue) };
1135-
}
1131+
// 2c. If type is mapped with placeholder, use the "type with maybe Omit" as {T}
1132+
if (isTypeMapped && hasPlaceholder(isTypeMapped.type)) {
1133+
return { typename: type.name, typeValue: replacePlaceholder(isTypeMapped.type, typeValue) };
1134+
}
11361135

1137-
// 2d. If has default mapper with placeholder, use the "type with maybe Omit" as {T}
1138-
const hasDefaultMapper = !!this.config.defaultMapper?.type;
1139-
const isScalar = this.config.scalars[typeName];
1140-
if (hasDefaultMapper && hasPlaceholder(this.config.defaultMapper.type)) {
1141-
const finalTypename = isScalar ? this._getScalar(typeName) : typeValue;
1142-
return {
1143-
typename: type.name,
1144-
typeValue: replacePlaceholder(this.config.defaultMapper.type, finalTypename),
1145-
};
1146-
}
1136+
// 2d. If has default mapper with placeholder, use the "type with maybe Omit" as {T}
1137+
const hasDefaultMapper = !!this.config.defaultMapper?.type;
1138+
const isScalar = this.config.scalars[typeName];
1139+
if (hasDefaultMapper && hasPlaceholder(this.config.defaultMapper.type)) {
1140+
const finalTypename = isScalar ? this._getScalar(typeName) : typeValue;
1141+
return {
1142+
typename: type.name,
1143+
typeValue: replacePlaceholder(this.config.defaultMapper.type, finalTypename),
1144+
};
1145+
}
11471146

1148-
return { typename: type.name, typeValue };
1149-
})
1150-
.map(({ typename, typeValue }) => {
1151-
const nonOptionalTypenameModifier = isTypenameNonOptional ? ` & { __typename: '${typename}' }` : '';
1147+
return { typename: type.name, typeValue };
1148+
})
1149+
.map(({ typename, typeValue }) => {
1150+
const nonOptionalTypenameModifier = isTypenameNonOptional ? ` & { __typename: '${typename}' }` : '';
11521151

1153-
return `( ${typeValue}${nonOptionalTypenameModifier} )`; // Must wrap every type in explicit "( )" to separate them
1154-
})
1155-
.join(' | ') || 'never';
1152+
return `( ${typeValue}${nonOptionalTypenameModifier} )`; // Must wrap every type in explicit "( )" to separate them
1153+
});
1154+
const result =
1155+
members.length === 0
1156+
? 'never'
1157+
: members.length > 1
1158+
? `\n | ${members.map(m => m.replace(/\n/g, '\n ')).join('\n | ')}\n `
1159+
: members.join(' | ');
11561160
return result;
11571161
}
11581162

packages/plugins/other/visitor-plugin-common/src/selection-set-to-object.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -844,7 +844,7 @@ export class SelectionSetToObject<Config extends ParsedDocumentsConfig = ParsedD
844844
this.getEmptyObjectTypeString(mustAddEmptyObject),
845845
].filter(Boolean);
846846

847-
const content = typeParts.join(' | ');
847+
const content = formatUnion(typeParts);
848848

849849
if (typeParts.length > 1 && this._config.extractAllFieldsToTypes) {
850850
return {
@@ -944,7 +944,7 @@ export class SelectionSetToObject<Config extends ParsedDocumentsConfig = ParsedD
944944
.export()
945945
.asKind('type')
946946
.withName(mergedTypeString)
947-
.withContent(subTypes.map(t => t.name).join(' | ')).string,
947+
.withContent(formatUnion(subTypes.map(t => t.name))).string,
948948
].join('\n');
949949
}
950950

@@ -961,3 +961,10 @@ export class SelectionSetToObject<Config extends ParsedDocumentsConfig = ParsedD
961961
return operationTypes.includes(typeName) ? parentName : `${parentName}_${typeName}`;
962962
}
963963
}
964+
965+
function formatUnion(members: string[]): string {
966+
if (members.length > 1) {
967+
return `\n | ${members.map(m => m.replace(/\n/g, '\n ')).join('\n | ')}\n`;
968+
}
969+
return members.join(' | ');
970+
}

packages/plugins/typescript/operations/src/ts-selection-set-processor.ts

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,20 @@ export class TypeScriptSelectionSetProcessor extends BaseSelectionSetProcessor<S
2525
});
2626

2727
if (unsetTypes) {
28-
return [`MakeEmpty<${parentName}, ${fields.map(field => `'${field.fieldName}'`).join(' | ')}>`];
28+
const escapedFieldNames = fields.map(field => `'${field.fieldName}'`);
29+
return [formattedUnionTransform('MakeEmpty', parentName, escapedFieldNames)];
2930
}
3031

3132
let hasConditionals = false;
32-
const conditilnalsList: string[] = [];
33-
let resString = `Pick<${parentName}, ${fields
34-
.map(field => {
35-
if (field.isConditional) {
36-
hasConditionals = true;
37-
conditilnalsList.push(field.fieldName);
38-
}
39-
return `'${field.fieldName}'`;
40-
})
41-
.join(' | ')}>`;
33+
const escapedConditionalsList: string[] = [];
34+
const escapedFieldNames = fields.map(field => {
35+
if (field.isConditional) {
36+
hasConditionals = true;
37+
escapedConditionalsList.push(`'${field.fieldName}'`);
38+
}
39+
return `'${field.fieldName}'`;
40+
});
41+
let resString = formattedUnionTransform('Pick', parentName, escapedFieldNames);
4242

4343
if (hasConditionals) {
4444
const avoidOptional =
@@ -52,7 +52,7 @@ export class TypeScriptSelectionSetProcessor extends BaseSelectionSetProcessor<S
5252
const transform = avoidOptional ? 'MakeMaybe' : 'MakeOptional';
5353
resString = `${
5454
this.config.namespacedImportName ? `${this.config.namespacedImportName}.` : ''
55-
}${transform}<${resString}, ${conditilnalsList.map(field => `'${field}'`).join(' | ')}>`;
55+
}${formattedUnionTransform(transform, resString, escapedConditionalsList)}`;
5656
}
5757
return [resString];
5858
}
@@ -75,25 +75,38 @@ export class TypeScriptSelectionSetProcessor extends BaseSelectionSetProcessor<S
7575
useTypesPrefix: true,
7676
});
7777

78-
return [
79-
`{ ${fields
80-
.map(aliasedField => {
81-
const value =
82-
aliasedField.fieldName === '__typename'
83-
? `'${schemaType.name}'`
84-
: `${parentName}['${aliasedField.fieldName}']`;
85-
86-
return `${aliasedField.alias}: ${value}`;
87-
})
88-
.join(', ')} }`,
89-
];
78+
const selections = fields.map(aliasedField => {
79+
const value =
80+
aliasedField.fieldName === '__typename' ? `'${schemaType.name}'` : `${parentName}['${aliasedField.fieldName}']`;
81+
82+
return `${aliasedField.alias}: ${value}`;
83+
});
84+
return [formatSelections(selections)];
9085
}
9186

9287
transformLinkFields(fields: LinkField[]): ProcessResult {
9388
if (fields.length === 0) {
9489
return [];
9590
}
9691

97-
return [`{ ${fields.map(field => `${field.alias || field.name}: ${field.selectionSet}`).join(', ')} }`];
92+
const selections = fields.map(field => `${field.alias || field.name}: ${field.selectionSet}`);
93+
94+
return [formatSelections(selections)];
95+
}
96+
}
97+
98+
/** Equivalent to `${transformName}<${target}, ${unionElements.join(' | ')}>`, but with line feeds if necessary */
99+
function formattedUnionTransform(transformName: string, target: string, unionElements: string[]): string {
100+
if (unionElements.length > 3) {
101+
return `${transformName}<\n ${target},\n | ${unionElements.join('\n | ')}\n >`;
102+
}
103+
return `${transformName}<${target}, ${unionElements.join(' | ')}>`;
104+
}
105+
106+
/** Equivalent to `{ ${selections.join(', ')} }`, but with line feeds if necessary */
107+
function formatSelections(selections: string[]): string {
108+
if (selections.length > 1) {
109+
return `{\n ${selections.map(s => s.replace(/\n/g, '\n ')).join(',\n ')},\n }`;
98110
}
111+
return `{ ${selections.join(', ')} }`;
99112
}

0 commit comments

Comments
 (0)