1 | "use strict";
|
2 |
|
3 | const fs = require(`fs-extra`);
|
4 |
|
5 | const {
|
6 | EnumTypeComposer,
|
7 | InputTypeComposer,
|
8 | InterfaceTypeComposer,
|
9 | ObjectTypeComposer,
|
10 | ScalarTypeComposer,
|
11 | UnionTypeComposer
|
12 | } = require(`graphql-compose`);
|
13 |
|
14 | const report = require(`gatsby-cli/lib/reporter`);
|
15 |
|
16 | const {
|
17 | internalExtensionNames
|
18 | } = require(`./extensions`);
|
19 |
|
20 | const printTypeDefinitions = ({
|
21 | config,
|
22 | schemaComposer
|
23 | }) => {
|
24 | if (!config) return Promise.resolve();
|
25 | const {
|
26 | path,
|
27 | include = {},
|
28 | exclude = {},
|
29 | withFieldTypes
|
30 | } = config || {};
|
31 |
|
32 | if (!path) {
|
33 | report.error(`Printing type definitions aborted. Please provide a file path.`);
|
34 | return Promise.resolve();
|
35 | }
|
36 |
|
37 | if (fs.existsSync(path)) {
|
38 | report.error(`Printing type definitions aborted. The file \`${path}\` already exists.`);
|
39 | return Promise.resolve();
|
40 | }
|
41 |
|
42 | const internalTypes = [`Boolean`, `Buffer`, `Date`, `Float`, `ID`, `Int`, `Internal`, `InternalInput`, `JSON`, `Json`, `Node`, `NodeInput`, `Query`, `String`];
|
43 | const internalPlugins = [`internal-data-bridge`];
|
44 | const typesToExclude = exclude.types || [];
|
45 | const pluginsToExclude = exclude.plugins || [];
|
46 |
|
47 | const getName = tc => tc.name || tc.getTypeName();
|
48 |
|
49 | const isInternalType = tc => {
|
50 | const typeName = getName(tc);
|
51 |
|
52 | if (internalTypes.includes(typeName)) {
|
53 | return true;
|
54 | }
|
55 |
|
56 | const plugin = tc.getExtension(`plugin`);
|
57 |
|
58 | if (internalPlugins.includes(plugin)) {
|
59 | return true;
|
60 | }
|
61 |
|
62 | return false;
|
63 | };
|
64 |
|
65 | const shouldIncludeType = tc => {
|
66 | const typeName = getName(tc);
|
67 |
|
68 | if (typesToExclude.includes(typeName)) {
|
69 | return false;
|
70 | }
|
71 |
|
72 | if (include.types && !include.types.includes(typeName)) {
|
73 | return false;
|
74 | }
|
75 |
|
76 | const plugin = tc.getExtension(`plugin`);
|
77 |
|
78 | if (pluginsToExclude.includes(plugin)) {
|
79 | return false;
|
80 | }
|
81 |
|
82 | if (include.plugins && !include.plugins.includes(plugin)) {
|
83 | return false;
|
84 | }
|
85 |
|
86 | return true;
|
87 | };
|
88 |
|
89 |
|
90 |
|
91 |
|
92 | const processedTypes = new Set();
|
93 | const typeDefs = new Set();
|
94 |
|
95 | const addType = tc => {
|
96 | const typeName = getName(tc);
|
97 |
|
98 | if (!processedTypes.has(typeName) && !isInternalType(tc)) {
|
99 | processedTypes.add(typeName);
|
100 | return typeDefs.add(tc);
|
101 | }
|
102 |
|
103 | processedTypes.add(typeName);
|
104 | return null;
|
105 | };
|
106 |
|
107 | const addWithFieldTypes = tc => {
|
108 | if (addType(tc) && (tc instanceof ObjectTypeComposer || tc instanceof InterfaceTypeComposer || tc instanceof InputTypeComposer)) {
|
109 | if (tc instanceof ObjectTypeComposer) {
|
110 | const interfaces = tc.getInterfaces();
|
111 | interfaces.forEach(iface => {
|
112 | const ifaceName = getName(iface);
|
113 |
|
114 | if (ifaceName !== `Node`) {
|
115 | addWithFieldTypes(schemaComposer.getAnyTC(ifaceName));
|
116 | }
|
117 | });
|
118 | }
|
119 |
|
120 | tc.getFieldNames().forEach(fieldName => {
|
121 | const fieldType = tc.getFieldTC(fieldName);
|
122 | addWithFieldTypes(fieldType);
|
123 |
|
124 | if (!(tc instanceof InputTypeComposer)) {
|
125 | const fieldArgs = tc.getFieldArgs(fieldName);
|
126 | Object.keys(fieldArgs).forEach(argName => {
|
127 | addWithFieldTypes(tc.getFieldArgTC(fieldName, argName));
|
128 | });
|
129 | }
|
130 | });
|
131 | }
|
132 | };
|
133 |
|
134 | schemaComposer.forEach(tc => {
|
135 | if (!isInternalType(tc) && shouldIncludeType(tc)) {
|
136 | if (withFieldTypes) {
|
137 | addWithFieldTypes(tc);
|
138 | } else {
|
139 | addType(tc);
|
140 | }
|
141 | }
|
142 | });
|
143 | const printedTypeDefs = [`### Type definitions saved at ${new Date().toISOString()} ###`];
|
144 |
|
145 | try {
|
146 | typeDefs.forEach(tc => printedTypeDefs.push(printType(tc)));
|
147 | report.info(`Writing GraphQL type definitions to ${path}`);
|
148 | return fs.writeFile(path, printedTypeDefs.join(`\n\n`));
|
149 | } catch (error) {
|
150 | report.error(`Failed writing type definitions to \`${path}\`.`, error);
|
151 | return Promise.resolve();
|
152 | }
|
153 | };
|
154 |
|
155 | const printType = (tc, typeName) => {
|
156 | if (tc instanceof ObjectTypeComposer) {
|
157 | return printObjectType(tc);
|
158 | } else if (tc instanceof InterfaceTypeComposer) {
|
159 | return printInterfaceType(tc);
|
160 | } else if (tc instanceof UnionTypeComposer) {
|
161 | return printUnionType(tc);
|
162 | } else if (tc instanceof EnumTypeComposer) {
|
163 | return printEnumType(tc);
|
164 | } else if (tc instanceof ScalarTypeComposer) {
|
165 | return printScalarType(tc);
|
166 | } else if (tc instanceof InputTypeComposer) {
|
167 | return printInputObjectType(tc);
|
168 | } else {
|
169 | throw new Error(`Did not recognize type of ${typeName}.`);
|
170 | }
|
171 | };
|
172 |
|
173 |
|
174 | const {
|
175 | astFromValue,
|
176 | print,
|
177 | GraphQLString,
|
178 | DEFAULT_DEPRECATION_REASON
|
179 | } = require(`graphql`);
|
180 |
|
181 | const {
|
182 | printBlockString
|
183 | } = require(`graphql/language/blockString`);
|
184 |
|
185 | const _ = require(`lodash`);
|
186 |
|
187 | const printScalarType = tc => {
|
188 | const type = tc.getType();
|
189 | return printDescription(type) + `scalar ${type.name}`;
|
190 | };
|
191 |
|
192 | const printObjectType = tc => {
|
193 | const type = tc.getType();
|
194 | const interfaces = type.getInterfaces();
|
195 | const implementedInterfaces = interfaces.length ? ` implements ` + interfaces.map(i => i.name).join(` & `) : ``;
|
196 | const extensions = tc.getExtensions();
|
197 |
|
198 | if (tc.hasInterface(`Node`)) {
|
199 | extensions.dontInfer = null;
|
200 | }
|
201 |
|
202 | const directives = tc.schemaComposer.getDirectives();
|
203 | const printedDirectives = printDirectives(extensions, directives);
|
204 | const fields = tc.hasInterface(`Node`) ? Object.values(type.getFields()).filter(field => ![`id`, `parent`, `children`, `internal`].includes(field.name)) : Object.values(type.getFields());
|
205 | return printDescription(type) + `type ${type.name}${implementedInterfaces}${printedDirectives}` + printFields(fields, directives);
|
206 | };
|
207 |
|
208 | const printInterfaceType = tc => {
|
209 | const type = tc.getType();
|
210 | const extensions = tc.getExtensions();
|
211 | const directives = tc.schemaComposer.getDirectives();
|
212 | const printedDirectives = printDirectives(extensions, directives);
|
213 | return printDescription(type) + `interface ${type.name}${printedDirectives}` + printFields(Object.values(type.getFields()), directives);
|
214 | };
|
215 |
|
216 | const printUnionType = tc => {
|
217 | const type = tc.getType();
|
218 | const types = type.getTypes();
|
219 | const possibleTypes = types.length ? ` = ` + types.join(` | `) : ``;
|
220 | return printDescription(type) + `union ` + type.name + possibleTypes;
|
221 | };
|
222 |
|
223 | const printEnumType = tc => {
|
224 | const type = tc.getType();
|
225 | const values = type.getValues().map((value, i) => printDescription(value, ` `, !i) + ` ` + value.name + printDeprecated(value));
|
226 | return printDescription(type) + `enum ${type.name}` + printBlock(values);
|
227 | };
|
228 |
|
229 | const printInputObjectType = tc => {
|
230 | const type = tc.getType();
|
231 | const fields = Object.values(type.getFields()).map((f, i) => printDescription(f, ` `, !i) + ` ` + printInputValue(f));
|
232 | return printDescription(type) + `input ${type.name}` + printBlock(fields);
|
233 | };
|
234 |
|
235 | const printFields = (fields, directives) => {
|
236 | const printedFields = fields.map((f, i) => printDescription(f, ` `, !i) + ` ` + f.name + printArgs(f.args, ` `) + `: ` + String(f.type) + printDirectives(f.extensions || {}, directives) + printDeprecated(f));
|
237 | return printBlock(printedFields);
|
238 | };
|
239 |
|
240 | const printBlock = items => items.length !== 0 ? ` {\n` + items.join(`\n`) + `\n}` : ``;
|
241 |
|
242 | const printArgs = (args, indentation = ``) => {
|
243 | if (args.length === 0) {
|
244 | return ``;
|
245 | }
|
246 |
|
247 |
|
248 | if (args.every(arg => !arg.description)) {
|
249 | return `(` + args.map(printInputValue).join(`, `) + `)`;
|
250 | }
|
251 |
|
252 | return `(\n` + args.map((arg, i) => printDescription(arg, ` ` + indentation, !i) + ` ` + indentation + printInputValue(arg)).join(`\n`) + `\n` + indentation + `)`;
|
253 | };
|
254 |
|
255 | const printInputValue = arg => {
|
256 | const defaultAST = astFromValue(arg.defaultValue, arg.type);
|
257 | let argDecl = arg.name + `: ` + String(arg.type);
|
258 |
|
259 | if (defaultAST) {
|
260 | argDecl += ` = ${print(defaultAST)}`;
|
261 | }
|
262 |
|
263 | return argDecl;
|
264 | };
|
265 |
|
266 | const printDirectives = (extensions, directives) => Object.entries(extensions).map(([name, args]) => {
|
267 | if ([...internalExtensionNames, `deprecated`].includes(name)) return ``;
|
268 | return ` @${name}` + printDirectiveArgs(args, directives.find(directive => directive.name === name));
|
269 | }).join(``);
|
270 |
|
271 | const printDirectiveArgs = (args, directive) => {
|
272 | if (!args || !directive) {
|
273 | return ``;
|
274 | }
|
275 |
|
276 | const directiveArgs = Object.entries(args);
|
277 |
|
278 | if (directiveArgs.length === 0) {
|
279 | return ``;
|
280 | }
|
281 |
|
282 | return `(` + directiveArgs.map(([name, value]) => {
|
283 | const arg = directive.args && directive.args.find(arg => arg.name === name);
|
284 | return arg && `${name}: ${print(astFromValue(value, arg.type))}`;
|
285 | }).join(`, `) + `)`;
|
286 | };
|
287 |
|
288 | const printDeprecated = fieldOrEnumVal => {
|
289 | if (!fieldOrEnumVal.isDeprecated) {
|
290 | return ``;
|
291 | }
|
292 |
|
293 | const reason = fieldOrEnumVal.deprecationReason;
|
294 | const reasonAST = astFromValue(reason, GraphQLString);
|
295 |
|
296 | if (reasonAST && reason !== `` && reason !== DEFAULT_DEPRECATION_REASON) {
|
297 | return ` @deprecated(reason: ` + print(reasonAST) + `)`;
|
298 | }
|
299 |
|
300 | return ` @deprecated`;
|
301 | };
|
302 |
|
303 | const printDescription = (def, indentation = ``, firstInBlock = true) => {
|
304 | if (!def.description) {
|
305 | return ``;
|
306 | }
|
307 |
|
308 | const lines = descriptionLines(def.description, 120 - indentation.length);
|
309 | const text = lines.join(`\n`);
|
310 | const preferMultipleLines = text.length > 70;
|
311 | const blockString = printBlockString(text, ``, preferMultipleLines);
|
312 | const prefix = indentation && !firstInBlock ? `\n` + indentation : indentation;
|
313 | return prefix + blockString.replace(/\n/g, `\n` + indentation) + `\n`;
|
314 | };
|
315 |
|
316 | const descriptionLines = (description, maxLen) => {
|
317 | const rawLines = description.split(`\n`);
|
318 | return _.flatMap(rawLines, line => {
|
319 | if (line.length < maxLen + 5) {
|
320 | return line;
|
321 | }
|
322 |
|
323 |
|
324 |
|
325 | return breakLine(line, maxLen);
|
326 | });
|
327 | };
|
328 |
|
329 | const breakLine = (line, maxLen) => {
|
330 | const parts = line.split(new RegExp(`((?: |^).{15,${maxLen - 40}}(?= |$))`));
|
331 |
|
332 | if (parts.length < 4) {
|
333 | return [line];
|
334 | }
|
335 |
|
336 | const sublines = [parts[0] + parts[1] + parts[2]];
|
337 |
|
338 | for (let i = 3; i < parts.length; i += 2) {
|
339 | sublines.push(parts[i].slice(1) + parts[i + 1]);
|
340 | }
|
341 |
|
342 | return sublines;
|
343 | };
|
344 |
|
345 | module.exports = {
|
346 | printTypeDefinitions
|
347 | };
|
348 |
|
\ | No newline at end of file |