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): string {
39 return JSON.stringify(string);
40}
41
42function printBlockString(string: 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 && node.selectionSet.selections.length) {
89 out += ' ' + nodes.SelectionSet(node.selectionSet);
90 }
91 return out;
92 },
93 StringValue(node: StringValueNode): string {
94 if (node.block) {
95 return printBlockString(node.value).replace(/\n/g, LF);
96 } else {
97 return printString(node.value);
98 }
99 },
100 BooleanValue(node: BooleanValueNode): string {
101 return '' + node.value;
102 },
103 NullValue(_node: NullValueNode): string {
104 return 'null';
105 },
106 IntValue(node: IntValueNode): string {
107 return node.value;
108 },
109 FloatValue(node: FloatValueNode): string {
110 return node.value;
111 },
112 EnumValue(node: EnumValueNode): string {
113 return node.value;
114 },
115 Name(node: NameNode): string {
116 return node.value;
117 },
118 Variable(node: VariableNode): string {
119 return '$' + node.name.value;
120 },
121 ListValue(node: ListValueNode): string {
122 return '[' + mapJoin(node.values, ', ', _print) + ']';
123 },
124 ObjectValue(node: ObjectValueNode): string {
125 return '{' + mapJoin(node.fields, ', ', nodes.ObjectField) + '}';
126 },
127 ObjectField(node: ObjectFieldNode): string {
128 return node.name.value + ': ' + _print(node.value);
129 },
130 Document(node: DocumentNode): string {
131 if (!node.definitions || !node.definitions.length) return '';
132 return mapJoin(node.definitions, '\n\n', _print);
133 },
134 SelectionSet(node: SelectionSetNode): string {
135 return '{' + (LF += ' ') + mapJoin(node.selections, LF, _print) + (LF = LF.slice(0, -2)) + '}';
136 },
137 Argument(node: ArgumentNode): string {
138 return node.name.value + ': ' + _print(node.value);
139 },
140 FragmentSpread(node: FragmentSpreadNode): string {
141 let out = '...' + node.name.value;
142 if (node.directives && node.directives.length)
143 out += ' ' + mapJoin(node.directives, ' ', nodes.Directive);
144 return out;
145 },
146 InlineFragment(node: InlineFragmentNode): string {
147 let out = '...';
148 if (node.typeCondition) out += ' on ' + node.typeCondition.name.value;
149 if (node.directives && node.directives.length)
150 out += ' ' + mapJoin(node.directives, ' ', nodes.Directive);
151 out += ' ' + nodes.SelectionSet(node.selectionSet);
152 return out;
153 },
154 FragmentDefinition(node: FragmentDefinitionNode): string {
155 let out = 'fragment ' + node.name.value;
156 out += ' on ' + node.typeCondition.name.value;
157 if (node.directives && node.directives.length)
158 out += ' ' + mapJoin(node.directives, ' ', nodes.Directive);
159 return out + ' ' + nodes.SelectionSet(node.selectionSet);
160 },
161 Directive(node: DirectiveNode): string {
162 let out = '@' + node.name.value;
163 if (node.arguments && node.arguments.length)
164 out += '(' + mapJoin(node.arguments, ', ', nodes.Argument) + ')';
165 return out;
166 },
167 NamedType(node: NamedTypeNode): string {
168 return node.name.value;
169 },
170 ListType(node: ListTypeNode): string {
171 return '[' + _print(node.type) + ']';
172 },
173 NonNullType(node: NonNullTypeNode): string {
174 return _print(node.type) + '!';
175 },
176} as const;
177
178const _print = (node: ASTNode): string => nodes[node.kind](node);
179
180function print(node: ASTNode): string {
181 LF = '\n';
182 return nodes[node.kind] ? nodes[node.kind](node) : '';
183}
184
185export { print, printString, printBlockString };