Mirror: The spec-compliant minimum of client-side GraphQL.

refactor: Improve `print` performance (#24)

* Fix incorrect AST alias

* Update printer with new indent logic

* Simplify OperationDefinition printer

* Replace selection set generic printers

* Fix unrelated bench name

* Replace map + join

* Remove variableDefinitions that were accidentally copied over

* Add changeset

authored by kitten.sh and committed by GitHub 4f3e17a2 896f588a

Changed files
+133 -78
.changeset
src
+5
.changeset/three-buttons-eat.md
··· 1 + --- 2 + '@0no-co/graphql.web': patch 3 + --- 4 + 5 + Improve printer performance.
+1 -1
src/__tests__/visitor.bench.ts
··· 7 7 import kitchenSinkAST from './fixtures/kitchen_sink.json'; 8 8 import { visit } from '../visitor'; 9 9 10 - describe('print (kitchen sink AST)', () => { 10 + describe('visit (kitchen sink AST)', () => { 11 11 bench('@0no-co/graphql.web', () => { 12 12 visit(kitchenSinkAST, { 13 13 Field: formatNode,
+1 -1
src/ast.ts
··· 260 260 >; 261 261 262 262 export type StringValueNode = Or< 263 - GraphQL.FloatValueNode, 263 + GraphQL.StringValueNode, 264 264 { 265 265 readonly kind: Kind.STRING; 266 266 readonly value: string;
+126 -76
src/printer.ts
··· 1 - import type { ASTNode } from './ast'; 1 + import type { 2 + ASTNode, 3 + NameNode, 4 + DocumentNode, 5 + VariableNode, 6 + SelectionSetNode, 7 + FieldNode, 8 + ArgumentNode, 9 + FragmentSpreadNode, 10 + InlineFragmentNode, 11 + VariableDefinitionNode, 12 + OperationDefinitionNode, 13 + FragmentDefinitionNode, 14 + IntValueNode, 15 + FloatValueNode, 16 + StringValueNode, 17 + BooleanValueNode, 18 + NullValueNode, 19 + EnumValueNode, 20 + ListValueNode, 21 + ObjectValueNode, 22 + ObjectFieldNode, 23 + DirectiveNode, 24 + NamedTypeNode, 25 + ListTypeNode, 26 + NonNullTypeNode, 27 + } from './ast'; 28 + 29 + function mapJoin<T>(value: readonly T[], joiner: string, mapper: (value: T) => string): string { 30 + let out = ''; 31 + for (let index = 0; index < value.length; index++) { 32 + if (index) out += joiner; 33 + out += mapper(value[index]); 34 + } 35 + return out; 36 + } 2 37 3 - export function printString(string: string) { 38 + function printString(string: string) { 4 39 return JSON.stringify(string); 5 40 } 6 41 7 - export function printBlockString(string: string) { 42 + function printBlockString(string: string) { 8 43 return '"""\n' + string.replace(/"""/g, '\\"""') + '\n"""'; 9 44 } 10 45 11 - const hasItems = <T>(array: ReadonlyArray<T> | undefined | null): array is ReadonlyArray<T> => 12 - !!(array && array.length); 13 - 14 46 const MAX_LINE_LENGTH = 80; 15 47 16 - const nodes: { 17 - [NodeT in ASTNode as NodeT['kind']]?: (node: NodeT) => string; 18 - } = { 19 - OperationDefinition(node) { 20 - if ( 21 - node.operation === 'query' && 22 - !node.name && 23 - !hasItems(node.variableDefinitions) && 24 - !hasItems(node.directives) 25 - ) { 26 - return nodes.SelectionSet!(node.selectionSet); 27 - } 48 + let LF = '\n'; 49 + 50 + const nodes = { 51 + OperationDefinition(node: OperationDefinitionNode): string { 28 52 let out: string = node.operation; 29 53 if (node.name) out += ' ' + node.name.value; 30 - if (hasItems(node.variableDefinitions)) { 54 + if (node.variableDefinitions && node.variableDefinitions.length) { 31 55 if (!node.name) out += ' '; 32 - out += '(' + node.variableDefinitions.map(nodes.VariableDefinition!).join(', ') + ')'; 56 + out += '(' + mapJoin(node.variableDefinitions, ', ', nodes.VariableDefinition) + ')'; 33 57 } 34 - if (hasItems(node.directives)) out += ' ' + node.directives.map(nodes.Directive!).join(' '); 35 - return out + ' ' + nodes.SelectionSet!(node.selectionSet); 58 + if (node.directives && node.directives.length) 59 + out += ' ' + mapJoin(node.directives, ' ', nodes.Directive); 60 + return out !== 'query' 61 + ? out + ' ' + nodes.SelectionSet(node.selectionSet) 62 + : nodes.SelectionSet(node.selectionSet); 36 63 }, 37 - VariableDefinition(node) { 38 - let out = nodes.Variable!(node.variable) + ': ' + print(node.type); 39 - if (node.defaultValue) out += ' = ' + print(node.defaultValue); 40 - if (hasItems(node.directives)) out += ' ' + node.directives.map(nodes.Directive!).join(' '); 64 + VariableDefinition(node: VariableDefinitionNode): string { 65 + let out = nodes.Variable!(node.variable) + ': ' + _print(node.type); 66 + if (node.defaultValue) out += ' = ' + _print(node.defaultValue); 67 + if (node.directives && node.directives.length) 68 + out += ' ' + mapJoin(node.directives, ' ', nodes.Directive); 41 69 return out; 42 70 }, 43 - Field(node) { 44 - let out = (node.alias ? node.alias.value + ': ' : '') + node.name.value; 45 - if (hasItems(node.arguments)) { 46 - const args = node.arguments.map(nodes.Argument!); 47 - const argsLine = out + '(' + args.join(', ') + ')'; 48 - out = 49 - argsLine.length > MAX_LINE_LENGTH 50 - ? out + '(\n ' + args.join('\n').replace(/\n/g, '\n ') + '\n)' 51 - : argsLine; 71 + Field(node: FieldNode): string { 72 + let out = node.alias ? node.alias.value + ': ' + node.name.value : node.name.value; 73 + if (node.arguments && node.arguments.length) { 74 + const args = mapJoin(node.arguments, ', ', nodes.Argument); 75 + if (out.length + args.length + 2 > MAX_LINE_LENGTH) { 76 + out += 77 + '(' + 78 + (LF += ' ') + 79 + mapJoin(node.arguments, LF, nodes.Argument) + 80 + (LF = LF.slice(0, -2)) + 81 + ')'; 82 + } else { 83 + out += '(' + args + ')'; 84 + } 52 85 } 53 - if (hasItems(node.directives)) out += ' ' + node.directives.map(nodes.Directive!).join(' '); 54 - return node.selectionSet ? out + ' ' + nodes.SelectionSet!(node.selectionSet) : out; 86 + if (node.directives && node.directives.length) 87 + out += ' ' + mapJoin(node.directives, ' ', nodes.Directive); 88 + if (node.selectionSet) out += ' ' + nodes.SelectionSet(node.selectionSet); 89 + return out; 55 90 }, 56 - StringValue(node) { 57 - return node.block ? printBlockString(node.value) : printString(node.value); 91 + StringValue(node: StringValueNode): string { 92 + if (node.block) { 93 + return printBlockString(node.value).replace(/\n/g, LF); 94 + } else { 95 + return printString(node.value); 96 + } 58 97 }, 59 - BooleanValue(node) { 98 + BooleanValue(node: BooleanValueNode): string { 60 99 return '' + node.value; 61 100 }, 62 - NullValue(_node) { 101 + NullValue(_node: NullValueNode): string { 63 102 return 'null'; 64 103 }, 65 - IntValue(node) { 104 + IntValue(node: IntValueNode): string { 66 105 return node.value; 67 106 }, 68 - FloatValue(node) { 107 + FloatValue(node: FloatValueNode): string { 69 108 return node.value; 70 109 }, 71 - EnumValue(node) { 110 + EnumValue(node: EnumValueNode): string { 72 111 return node.value; 73 112 }, 74 - Name(node) { 113 + Name(node: NameNode): string { 75 114 return node.value; 76 115 }, 77 - Variable(node) { 116 + Variable(node: VariableNode): string { 78 117 return '$' + node.name.value; 79 118 }, 80 - ListValue(node) { 81 - return '[' + node.values.map(print).join(', ') + ']'; 119 + ListValue(node: ListValueNode): string { 120 + return '[' + mapJoin(node.values, ', ', _print) + ']'; 82 121 }, 83 - ObjectValue(node) { 84 - return '{' + node.fields.map(nodes.ObjectField!).join(', ') + '}'; 122 + ObjectValue(node: ObjectValueNode): string { 123 + return '{' + mapJoin(node.fields, ', ', nodes.ObjectField) + '}'; 85 124 }, 86 - ObjectField(node) { 87 - return node.name.value + ': ' + print(node.value); 125 + ObjectField(node: ObjectFieldNode): string { 126 + return node.name.value + ': ' + _print(node.value); 88 127 }, 89 - Document(node) { 90 - return hasItems(node.definitions) ? node.definitions.map(print).join('\n\n') : ''; 128 + Document(node: DocumentNode): string { 129 + if (!node.definitions || !node.definitions.length) return ''; 130 + return mapJoin(node.definitions, '\n\n', _print); 91 131 }, 92 - SelectionSet(node) { 93 - return '{\n ' + node.selections.map(print).join('\n').replace(/\n/g, '\n ') + '\n}'; 132 + SelectionSet(node: SelectionSetNode): string { 133 + return '{' + (LF += ' ') + mapJoin(node.selections, LF, _print) + (LF = LF.slice(0, -2)) + '}'; 94 134 }, 95 - Argument(node) { 96 - return node.name.value + ': ' + print(node.value); 135 + Argument(node: ArgumentNode): string { 136 + return node.name.value + ': ' + _print(node.value); 97 137 }, 98 - FragmentSpread(node) { 138 + FragmentSpread(node: FragmentSpreadNode): string { 99 139 let out = '...' + node.name.value; 100 - if (hasItems(node.directives)) out += ' ' + node.directives.map(nodes.Directive!).join(' '); 140 + if (node.directives && node.directives.length) 141 + out += ' ' + mapJoin(node.directives, ' ', nodes.Directive); 101 142 return out; 102 143 }, 103 - InlineFragment(node) { 144 + InlineFragment(node: InlineFragmentNode): string { 104 145 let out = '...'; 105 146 if (node.typeCondition) out += ' on ' + node.typeCondition.name.value; 106 - if (hasItems(node.directives)) out += ' ' + node.directives.map(nodes.Directive!).join(' '); 107 - return out + ' ' + print(node.selectionSet); 147 + if (node.directives && node.directives.length) 148 + out += ' ' + mapJoin(node.directives, ' ', nodes.Directive); 149 + out += ' ' + nodes.SelectionSet(node.selectionSet); 150 + return out; 108 151 }, 109 - FragmentDefinition(node) { 152 + FragmentDefinition(node: FragmentDefinitionNode): string { 110 153 let out = 'fragment ' + node.name.value; 111 154 out += ' on ' + node.typeCondition.name.value; 112 - if (hasItems(node.directives)) out += ' ' + node.directives.map(nodes.Directive!).join(' '); 113 - return out + ' ' + print(node.selectionSet); 155 + if (node.directives && node.directives.length) 156 + out += ' ' + mapJoin(node.directives, ' ', nodes.Directive); 157 + return out + ' ' + nodes.SelectionSet(node.selectionSet); 114 158 }, 115 - Directive(node) { 159 + Directive(node: DirectiveNode): string { 116 160 let out = '@' + node.name.value; 117 - if (hasItems(node.arguments)) out += '(' + node.arguments.map(nodes.Argument!).join(', ') + ')'; 161 + if (node.arguments && node.arguments.length) 162 + out += '(' + mapJoin(node.arguments, ', ', nodes.Argument) + ')'; 118 163 return out; 119 164 }, 120 - NamedType(node) { 165 + NamedType(node: NamedTypeNode): string { 121 166 return node.name.value; 122 167 }, 123 - ListType(node) { 124 - return '[' + print(node.type) + ']'; 168 + ListType(node: ListTypeNode): string { 169 + return '[' + _print(node.type) + ']'; 125 170 }, 126 - NonNullType(node) { 127 - return print(node.type) + '!'; 171 + NonNullType(node: NonNullTypeNode): string { 172 + return _print(node.type) + '!'; 128 173 }, 129 - }; 174 + } as const; 130 175 131 - export function print(node: ASTNode): string { 132 - return nodes[node.kind] ? (nodes as any)[node.kind]!(node) : ''; 176 + const _print = (node: ASTNode): string => nodes[node.kind](node); 177 + 178 + function print(node: ASTNode): string { 179 + LF = '\n'; 180 + return nodes[node.kind] ? nodes[node.kind](node) : ''; 133 181 } 182 + 183 + export { print, printString, printBlockString };