14.3 kBJavaScriptView Raw
1import { TokenKind, visit, } from 'graphql';
2const MAX_LINE_LENGTH = 80;
3let commentsRegistry = {};
4export function resetComments() {
5 commentsRegistry = {};
6}
7export function collectComment(node) {
8 const entityName = node.name?.value;
9 if (entityName == null) {
10 return;
11 }
12 pushComment(node, entityName);
13 switch (node.kind) {
14 case 'EnumTypeDefinition':
15 if (node.values) {
16 for (const value of node.values) {
17 pushComment(value, entityName, value.name.value);
18 }
19 }
20 break;
21 case 'ObjectTypeDefinition':
22 case 'InputObjectTypeDefinition':
23 case 'InterfaceTypeDefinition':
24 if (node.fields) {
25 for (const field of node.fields) {
26 pushComment(field, entityName, field.name.value);
27 if (isFieldDefinitionNode(field) && field.arguments) {
28 for (const arg of field.arguments) {
29 pushComment(arg, entityName, field.name.value, arg.name.value);
30 }
31 }
32 }
33 }
34 break;
35 }
36}
37export function pushComment(node, entity, field, argument) {
38 const comment = getComment(node);
39 if (typeof comment !== 'string' || comment.length === 0) {
40 return;
41 }
42 const keys = [entity];
43 if (field) {
44 keys.push(field);
45 if (argument) {
46 keys.push(argument);
47 }
48 }
49 const path = keys.join('.');
50 if (!commentsRegistry[path]) {
51 commentsRegistry[path] = [];
52 }
53 commentsRegistry[path].push(comment);
54}
55export function printComment(comment) {
56 return '\n# ' + comment.replace(/\n/g, '\n# ');
57}
58/**
59 * Copyright (c) 2015-present, Facebook, Inc.
60 *
61 * This source code is licensed under the MIT license found in the
62 * LICENSE file in the root directory of this source tree.
63 */
64/**
65 * NOTE: ==> This file has been modified just to add comments to the printed AST
66 * This is a temp measure, we will move to using the original non modified printer.js ASAP.
67 */
68/**
69 * Given maybeArray, print an empty string if it is null or empty, otherwise
70 * print all items together separated by separator if provided
71 */
72function join(maybeArray, separator) {
73 return maybeArray ? maybeArray.filter(x => x).join(separator || '') : '';
74}
75function hasMultilineItems(maybeArray) {
76 return maybeArray?.some(str => str.includes('\n')) ?? false;
77}
78function addDescription(cb) {
79 return (node, _key, _parent, path, ancestors) => {
80 const keys = [];
81 const parent = path.reduce((prev, key) => {
82 if (['fields', 'arguments', 'values'].includes(key) && prev.name) {
83 keys.push(prev.name.value);
84 }
85 return prev[key];
86 }, ancestors[0]);
87 const key = [...keys, parent?.name?.value].filter(Boolean).join('.');
88 const items = [];
89 if (node.kind.includes('Definition') && commentsRegistry[key]) {
90 items.push(...commentsRegistry[key]);
91 }
92 return join([...items.map(printComment), node.description, cb(node, _key, _parent, path, ancestors)], '\n');
93 };
94}
95function indent(maybeString) {
96 return maybeString && ` ${maybeString.replace(/\n/g, '\n ')}`;
97}
98/**
99 * Given array, print each item on its own line, wrapped in an
100 * indented "{ }" block.
101 */
102function block(array) {
103 return array && array.length !== 0 ? `{\n${indent(join(array, '\n'))}\n}` : '';
104}
105/**
106 * If maybeString is not null or empty, then wrap with start and end, otherwise
107 * print an empty string.
108 */
109function wrap(start, maybeString, end) {
110 return maybeString ? start + maybeString + (end || '') : '';
111}
112/**
113 * Print a block string in the indented block form by adding a leading and
114 * trailing blank line. However, if a block string starts with whitespace and is
115 * a single-line, adding a leading blank line would strip that whitespace.
116 */
117function printBlockString(value, isDescription = false) {
118 const escaped = value.replace(/"""/g, '\\"""');
119 return (value[0] === ' ' || value[0] === '\t') && value.indexOf('\n') === -1
120 ? `"""${escaped.replace(/"$/, '"\n')}"""`
121 : `"""\n${isDescription ? escaped : indent(escaped)}\n"""`;
122}
123const printDocASTReducer = {
124 Name: { leave: node => node.value },
125 Variable: { leave: node => '$' + node.name },
126 // Document
127 Document: {
128 leave: node => join(node.definitions, '\n\n'),
129 },
130 OperationDefinition: {
131 leave: node => {
132 const varDefs = wrap('(', join(node.variableDefinitions, ', '), ')');
133 const prefix = join([node.operation, join([node.name, varDefs]), join(node.directives, ' ')], ' ');
134 // the query short form.
135 return prefix + ' ' + node.selectionSet;
136 },
137 },
138 VariableDefinition: {
139 leave: ({ variable, type, defaultValue, directives }) => variable + ': ' + type + wrap(' = ', defaultValue) + wrap(' ', join(directives, ' ')),
140 },
141 SelectionSet: { leave: ({ selections }) => block(selections) },
142 Field: {
143 leave({ alias, name, arguments: args, directives, selectionSet }) {
144 const prefix = wrap('', alias, ': ') + name;
145 let argsLine = prefix + wrap('(', join(args, ', '), ')');
146 if (argsLine.length > MAX_LINE_LENGTH) {
147 argsLine = prefix + wrap('(\n', indent(join(args, '\n')), '\n)');
148 }
149 return join([argsLine, join(directives, ' '), selectionSet], ' ');
150 },
151 },
152 Argument: { leave: ({ name, value }) => name + ': ' + value },
153 // Fragments
154 FragmentSpread: {
155 leave: ({ name, directives }) => '...' + name + wrap(' ', join(directives, ' ')),
156 },
157 InlineFragment: {
158 leave: ({ typeCondition, directives, selectionSet }) => join(['...', wrap('on ', typeCondition), join(directives, ' '), selectionSet], ' '),
159 },
160 FragmentDefinition: {
161 leave: ({ name, typeCondition, variableDefinitions, directives, selectionSet }) =>
162 // Note: fragment variable definitions are experimental and may be changed
163 // or removed in the future.
164 `fragment ${name}${wrap('(', join(variableDefinitions, ', '), ')')} ` +
165 `on ${typeCondition} ${wrap('', join(directives, ' '), ' ')}` +
166 selectionSet,
167 },
168 // Value
169 IntValue: { leave: ({ value }) => value },
170 FloatValue: { leave: ({ value }) => value },
171 StringValue: {
172 leave: ({ value, block: isBlockString }) => {
173 if (isBlockString) {
174 return printBlockString(value);
175 }
176 return JSON.stringify(value);
177 },
178 },
179 BooleanValue: { leave: ({ value }) => (value ? 'true' : 'false') },
180 NullValue: { leave: () => 'null' },
181 EnumValue: { leave: ({ value }) => value },
182 ListValue: { leave: ({ values }) => '[' + join(values, ', ') + ']' },
183 ObjectValue: { leave: ({ fields }) => '{' + join(fields, ', ') + '}' },
184 ObjectField: { leave: ({ name, value }) => name + ': ' + value },
185 // Directive
186 Directive: {
187 leave: ({ name, arguments: args }) => '@' + name + wrap('(', join(args, ', '), ')'),
188 },
189 // Type
190 NamedType: { leave: ({ name }) => name },
191 ListType: { leave: ({ type }) => '[' + type + ']' },
192 NonNullType: { leave: ({ type }) => type + '!' },
193 // Type System Definitions
194 SchemaDefinition: {
195 leave: ({ directives, operationTypes }) => join(['schema', join(directives, ' '), block(operationTypes)], ' '),
196 },
197 OperationTypeDefinition: {
198 leave: ({ operation, type }) => operation + ': ' + type,
199 },
200 ScalarTypeDefinition: {
201 leave: ({ name, directives }) => join(['scalar', name, join(directives, ' ')], ' '),
202 },
203 ObjectTypeDefinition: {
204 leave: ({ name, interfaces, directives, fields }) => join([
205 'type',
206 name,
207 wrap('implements ', join(interfaces, ' & ')),
208 join(directives, ' '),
209 block(fields),
210 ], ' '),
211 },
212 FieldDefinition: {
213 leave: ({ name, arguments: args, type, directives }) => name +
214 (hasMultilineItems(args)
215 ? wrap('(\n', indent(join(args, '\n')), '\n)')
216 : wrap('(', join(args, ', '), ')')) +
217 ': ' +
218 type +
219 wrap(' ', join(directives, ' ')),
220 },
221 InputValueDefinition: {
222 leave: ({ name, type, defaultValue, directives }) => join([name + ': ' + type, wrap('= ', defaultValue), join(directives, ' ')], ' '),
223 },
224 InterfaceTypeDefinition: {
225 leave: ({ name, interfaces, directives, fields }) => join([
226 'interface',
227 name,
228 wrap('implements ', join(interfaces, ' & ')),
229 join(directives, ' '),
230 block(fields),
231 ], ' '),
232 },
233 UnionTypeDefinition: {
234 leave: ({ name, directives, types }) => join(['union', name, join(directives, ' '), wrap('= ', join(types, ' | '))], ' '),
235 },
236 EnumTypeDefinition: {
237 leave: ({ name, directives, values }) => join(['enum', name, join(directives, ' '), block(values)], ' '),
238 },
239 EnumValueDefinition: {
240 leave: ({ name, directives }) => join([name, join(directives, ' ')], ' '),
241 },
242 InputObjectTypeDefinition: {
243 leave: ({ name, directives, fields }) => join(['input', name, join(directives, ' '), block(fields)], ' '),
244 },
245 DirectiveDefinition: {
246 leave: ({ name, arguments: args, repeatable, locations }) => 'directive @' +
247 name +
248 (hasMultilineItems(args)
249 ? wrap('(\n', indent(join(args, '\n')), '\n)')
250 : wrap('(', join(args, ', '), ')')) +
251 (repeatable ? ' repeatable' : '') +
252 ' on ' +
253 join(locations, ' | '),
254 },
255 SchemaExtension: {
256 leave: ({ directives, operationTypes }) => join(['extend schema', join(directives, ' '), block(operationTypes)], ' '),
257 },
258 ScalarTypeExtension: {
259 leave: ({ name, directives }) => join(['extend scalar', name, join(directives, ' ')], ' '),
260 },
261 ObjectTypeExtension: {
262 leave: ({ name, interfaces, directives, fields }) => join([
263 'extend type',
264 name,
265 wrap('implements ', join(interfaces, ' & ')),
266 join(directives, ' '),
267 block(fields),
268 ], ' '),
269 },
270 InterfaceTypeExtension: {
271 leave: ({ name, interfaces, directives, fields }) => join([
272 'extend interface',
273 name,
274 wrap('implements ', join(interfaces, ' & ')),
275 join(directives, ' '),
276 block(fields),
277 ], ' '),
278 },
279 UnionTypeExtension: {
280 leave: ({ name, directives, types }) => join(['extend union', name, join(directives, ' '), wrap('= ', join(types, ' | '))], ' '),
281 },
282 EnumTypeExtension: {
283 leave: ({ name, directives, values }) => join(['extend enum', name, join(directives, ' '), block(values)], ' '),
284 },
285 InputObjectTypeExtension: {
286 leave: ({ name, directives, fields }) => join(['extend input', name, join(directives, ' '), block(fields)], ' '),
287 },
288};
289const printDocASTReducerWithComments = Object.keys(printDocASTReducer).reduce((prev, key) => ({
290 ...prev,
291 [key]: {
292 leave: addDescription(printDocASTReducer[key].leave),
293 },
294}), {});
295/**
296 * Converts an AST into a string, using one set of reasonable
297 * formatting rules.
298 */
299export function printWithComments(ast) {
300 return visit(ast, printDocASTReducerWithComments);
301}
302function isFieldDefinitionNode(node) {
303 return node.kind === 'FieldDefinition';
304}
305// graphql < v13 and > v15 does not export getDescription
306export function getDescription(node, options) {
307 if (node.description != null) {
308 return node.description.value;
309 }
310 if (options?.commentDescriptions) {
311 return getComment(node);
312 }
313}
314export function getComment(node) {
315 const rawValue = getLeadingCommentBlock(node);
316 if (rawValue !== undefined) {
317 return dedentBlockStringValue(`\n${rawValue}`);
318 }
319}
320export function getLeadingCommentBlock(node) {
321 const loc = node.loc;
322 if (!loc) {
323 return;
324 }
325 const comments = [];
326 let token = loc.startToken.prev;
327 while (token != null &&
328 token.kind === TokenKind.COMMENT &&
329 token.next != null &&
330 token.prev != null &&
331 token.line + 1 === token.next.line &&
332 token.line !== token.prev.line) {
333 const value = String(token.value);
334 comments.push(value);
335 token = token.prev;
336 }
337 return comments.length > 0 ? comments.reverse().join('\n') : undefined;
338}
339export function dedentBlockStringValue(rawString) {
340 // Expand a block string's raw value into independent lines.
341 const lines = rawString.split(/\r\n|[\n\r]/g);
342 // Remove common indentation from all lines but first.
343 const commonIndent = getBlockStringIndentation(lines);
344 if (commonIndent !== 0) {
345 for (let i = 1; i < lines.length; i++) {
346 lines[i] = lines[i].slice(commonIndent);
347 }
348 }
349 // Remove leading and trailing blank lines.
350 while (lines.length > 0 && isBlank(lines[0])) {
351 lines.shift();
352 }
353 while (lines.length > 0 && isBlank(lines[lines.length - 1])) {
354 lines.pop();
355 }
356 // Return a string of the lines joined with U+000A.
357 return lines.join('\n');
358}
359/**
360 * @internal
361 */
362export function getBlockStringIndentation(lines) {
363 let commonIndent = null;
364 for (let i = 1; i < lines.length; i++) {
365 const line = lines[i];
366 const indent = leadingWhitespace(line);
367 if (indent === line.length) {
368 continue; // skip empty lines
369 }
370 if (commonIndent === null || indent < commonIndent) {
371 commonIndent = indent;
372 if (commonIndent === 0) {
373 break;
374 }
375 }
376 }
377 return commonIndent === null ? 0 : commonIndent;
378}
379function leadingWhitespace(str) {
380 let i = 0;
381 while (i < str.length && (str[i] === ' ' || str[i] === '\t')) {
382 i++;
383 }
384 return i;
385}
386function isBlank(str) {
387 return leadingWhitespace(str) === str.length;
388}