Mirror: The spec-compliant minimum of client-side GraphQL.
1import 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
29function 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
38function printString(string: string) {
39 return JSON.stringify(string);
40}
41
42function printBlockString(string: string) {
43 return '"""\n' + string.replace(/"""/g, '\\"""') + '\n"""';
44}
45
46const MAX_LINE_LENGTH = 80;
47
48let LF = '\n';
49
50const nodes = {
51 OperationDefinition(node: OperationDefinitionNode): string {
52 let out: string = node.operation;
53 if (node.name) out += ' ' + node.name.value;
54 if (node.variableDefinitions && node.variableDefinitions.length) {
55 if (!node.name) out += ' ';
56 out += '(' + mapJoin(node.variableDefinitions, ', ', nodes.VariableDefinition) + ')';
57 }
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);
69 return out;
70 },
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 }
85 }
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;
90 },
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 }
97 },
98 BooleanValue(node: BooleanValueNode): string {
99 return '' + node.value;
100 },
101 NullValue(_node: NullValueNode): string {
102 return 'null';
103 },
104 IntValue(node: IntValueNode): string {
105 return node.value;
106 },
107 FloatValue(node: FloatValueNode): string {
108 return node.value;
109 },
110 EnumValue(node: EnumValueNode): string {
111 return node.value;
112 },
113 Name(node: NameNode): string {
114 return node.value;
115 },
116 Variable(node: VariableNode): string {
117 return '$' + node.name.value;
118 },
119 ListValue(node: ListValueNode): string {
120 return '[' + mapJoin(node.values, ', ', _print) + ']';
121 },
122 ObjectValue(node: ObjectValueNode): string {
123 return '{' + mapJoin(node.fields, ', ', nodes.ObjectField) + '}';
124 },
125 ObjectField(node: ObjectFieldNode): string {
126 return node.name.value + ': ' + _print(node.value);
127 },
128 Document(node: DocumentNode): string {
129 if (!node.definitions || !node.definitions.length) return '';
130 return mapJoin(node.definitions, '\n\n', _print);
131 },
132 SelectionSet(node: SelectionSetNode): string {
133 return '{' + (LF += ' ') + mapJoin(node.selections, LF, _print) + (LF = LF.slice(0, -2)) + '}';
134 },
135 Argument(node: ArgumentNode): string {
136 return node.name.value + ': ' + _print(node.value);
137 },
138 FragmentSpread(node: FragmentSpreadNode): string {
139 let out = '...' + node.name.value;
140 if (node.directives && node.directives.length)
141 out += ' ' + mapJoin(node.directives, ' ', nodes.Directive);
142 return out;
143 },
144 InlineFragment(node: InlineFragmentNode): string {
145 let out = '...';
146 if (node.typeCondition) out += ' on ' + node.typeCondition.name.value;
147 if (node.directives && node.directives.length)
148 out += ' ' + mapJoin(node.directives, ' ', nodes.Directive);
149 out += ' ' + nodes.SelectionSet(node.selectionSet);
150 return out;
151 },
152 FragmentDefinition(node: FragmentDefinitionNode): string {
153 let out = 'fragment ' + node.name.value;
154 out += ' on ' + node.typeCondition.name.value;
155 if (node.directives && node.directives.length)
156 out += ' ' + mapJoin(node.directives, ' ', nodes.Directive);
157 return out + ' ' + nodes.SelectionSet(node.selectionSet);
158 },
159 Directive(node: DirectiveNode): string {
160 let out = '@' + node.name.value;
161 if (node.arguments && node.arguments.length)
162 out += '(' + mapJoin(node.arguments, ', ', nodes.Argument) + ')';
163 return out;
164 },
165 NamedType(node: NamedTypeNode): string {
166 return node.name.value;
167 },
168 ListType(node: ListTypeNode): string {
169 return '[' + _print(node.type) + ']';
170 },
171 NonNullType(node: NonNullTypeNode): string {
172 return _print(node.type) + '!';
173 },
174} as const;
175
176const _print = (node: ASTNode): string => nodes[node.kind](node);
177
178function print(node: ASTNode): string {
179 LF = '\n';
180 return nodes[node.kind] ? nodes[node.kind](node) : '';
181}
182
183export { print, printString, printBlockString };