Skip to content

Commit 4f3e17a

Browse files
authored
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
1 parent 896f588 commit 4f3e17a

File tree

4 files changed

+134
-79
lines changed

4 files changed

+134
-79
lines changed

.changeset/three-buttons-eat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@0no-co/graphql.web': patch
3+
---
4+
5+
Improve printer performance.

src/__tests__/visitor.bench.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as graphql17 from 'graphql17';
77
import kitchenSinkAST from './fixtures/kitchen_sink.json';
88
import { visit } from '../visitor';
99

10-
describe('print (kitchen sink AST)', () => {
10+
describe('visit (kitchen sink AST)', () => {
1111
bench('@0no-co/graphql.web', () => {
1212
visit(kitchenSinkAST, {
1313
Field: formatNode,

src/ast.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ export type FloatValueNode = Or<
260260
>;
261261

262262
export type StringValueNode = Or<
263-
GraphQL.FloatValueNode,
263+
GraphQL.StringValueNode,
264264
{
265265
readonly kind: Kind.STRING;
266266
readonly value: string;

src/printer.ts

Lines changed: 127 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,133 +1,183 @@
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';
228

3-
export function printString(string: string) {
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+
}
37+
38+
function printString(string: string) {
439
return JSON.stringify(string);
540
}
641

7-
export function printBlockString(string: string) {
42+
function printBlockString(string: string) {
843
return '"""\n' + string.replace(/"""/g, '\\"""') + '\n"""';
944
}
1045

11-
const hasItems = <T>(array: ReadonlyArray<T> | undefined | null): array is ReadonlyArray<T> =>
12-
!!(array && array.length);
13-
1446
const MAX_LINE_LENGTH = 80;
1547

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 {
2852
let out: string = node.operation;
2953
if (node.name) out += ' ' + node.name.value;
30-
if (hasItems(node.variableDefinitions)) {
54+
if (node.variableDefinitions && node.variableDefinitions.length) {
3155
if (!node.name) out += ' ';
32-
out += '(' + node.variableDefinitions.map(nodes.VariableDefinition!).join(', ') + ')';
56+
out += '(' + mapJoin(node.variableDefinitions, ', ', nodes.VariableDefinition) + ')';
3357
}
34-
if (hasItems(node.directives)) out += ' ' + node.directives.map(nodes.Directive!).join(' ');
35-
return out + ' ' + nodes.SelectionSet!(node.selectionSet);
36-
},
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(' ');
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);
63+
},
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);
4169
return out;
4270
},
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+
}
5285
}
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;
5590
},
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+
}
5897
},
59-
BooleanValue(node) {
98+
BooleanValue(node: BooleanValueNode): string {
6099
return '' + node.value;
61100
},
62-
NullValue(_node) {
101+
NullValue(_node: NullValueNode): string {
63102
return 'null';
64103
},
65-
IntValue(node) {
104+
IntValue(node: IntValueNode): string {
66105
return node.value;
67106
},
68-
FloatValue(node) {
107+
FloatValue(node: FloatValueNode): string {
69108
return node.value;
70109
},
71-
EnumValue(node) {
110+
EnumValue(node: EnumValueNode): string {
72111
return node.value;
73112
},
74-
Name(node) {
113+
Name(node: NameNode): string {
75114
return node.value;
76115
},
77-
Variable(node) {
116+
Variable(node: VariableNode): string {
78117
return '$' + node.name.value;
79118
},
80-
ListValue(node) {
81-
return '[' + node.values.map(print).join(', ') + ']';
119+
ListValue(node: ListValueNode): string {
120+
return '[' + mapJoin(node.values, ', ', _print) + ']';
82121
},
83-
ObjectValue(node) {
84-
return '{' + node.fields.map(nodes.ObjectField!).join(', ') + '}';
122+
ObjectValue(node: ObjectValueNode): string {
123+
return '{' + mapJoin(node.fields, ', ', nodes.ObjectField) + '}';
85124
},
86-
ObjectField(node) {
87-
return node.name.value + ': ' + print(node.value);
125+
ObjectField(node: ObjectFieldNode): string {
126+
return node.name.value + ': ' + _print(node.value);
88127
},
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);
91131
},
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)) + '}';
94134
},
95-
Argument(node) {
96-
return node.name.value + ': ' + print(node.value);
135+
Argument(node: ArgumentNode): string {
136+
return node.name.value + ': ' + _print(node.value);
97137
},
98-
FragmentSpread(node) {
138+
FragmentSpread(node: FragmentSpreadNode): string {
99139
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);
101142
return out;
102143
},
103-
InlineFragment(node) {
144+
InlineFragment(node: InlineFragmentNode): string {
104145
let out = '...';
105146
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;
108151
},
109-
FragmentDefinition(node) {
152+
FragmentDefinition(node: FragmentDefinitionNode): string {
110153
let out = 'fragment ' + node.name.value;
111154
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);
114158
},
115-
Directive(node) {
159+
Directive(node: DirectiveNode): string {
116160
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) + ')';
118163
return out;
119164
},
120-
NamedType(node) {
165+
NamedType(node: NamedTypeNode): string {
121166
return node.name.value;
122167
},
123-
ListType(node) {
124-
return '[' + print(node.type) + ']';
168+
ListType(node: ListTypeNode): string {
169+
return '[' + _print(node.type) + ']';
125170
},
126-
NonNullType(node) {
127-
return print(node.type) + '!';
171+
NonNullType(node: NonNullTypeNode): string {
172+
return _print(node.type) + '!';
128173
},
129-
};
174+
} as const;
130175

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) : '';
133181
}
182+
183+
export { print, printString, printBlockString };

0 commit comments

Comments
 (0)