generate-graphql-docs.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. import fs from 'fs';
  2. import {
  3. buildClientSchema,
  4. GraphQLField,
  5. GraphQLInputObjectType,
  6. GraphQLNamedType,
  7. GraphQLObjectType,
  8. GraphQLType,
  9. GraphQLUnionType,
  10. isEnumType,
  11. isInputObjectType,
  12. isNamedType,
  13. isObjectType,
  14. isScalarType,
  15. isUnionType,
  16. } from 'graphql';
  17. import path from 'path';
  18. import { deleteGeneratedDocs, generateFrontMatter } from './docgen-utils';
  19. /* eslint-disable no-console */
  20. type TargetApi = 'shop' | 'admin';
  21. const targetApi: TargetApi = getTargetApiFromArgs();
  22. // The path to the introspection schema json file
  23. const SCHEMA_FILE = path.join(__dirname, `../../schema-${targetApi}.json`);
  24. // The absolute URL to the generated api docs section
  25. const docsUrl = `/docs/graphql-api/${targetApi}/`;
  26. // The directory in which the markdown files will be saved
  27. const outputPath = path.join(__dirname, `../../docs/content/graphql-api/${targetApi}`);
  28. const enum FileName {
  29. ENUM = 'enums',
  30. INPUT = 'input-types',
  31. MUTATION = 'mutations',
  32. QUERY = 'queries',
  33. OBJECT = 'object-types',
  34. }
  35. const schemaJson = fs.readFileSync(SCHEMA_FILE, 'utf8');
  36. const parsed = JSON.parse(schemaJson);
  37. const schema = buildClientSchema(parsed.data ? parsed.data : parsed);
  38. deleteGeneratedDocs(outputPath);
  39. generateGraphqlDocs(outputPath);
  40. function generateGraphqlDocs(hugoOutputPath: string) {
  41. const timeStart = +new Date();
  42. let queriesOutput = generateFrontMatter('Queries', 1) + '\n\n# Queries\n\n';
  43. let mutationsOutput = generateFrontMatter('Mutations', 2) + '\n\n# Mutations\n\n';
  44. let objectTypesOutput = generateFrontMatter('Types', 3) + '\n\n# Types\n\n';
  45. let inputTypesOutput = generateFrontMatter('Input Objects', 4) + '\n\n# Input Objects\n\n';
  46. let enumsOutput = generateFrontMatter('Enums', 5) + '\n\n# Enums\n\n';
  47. const sortByName = (a: { name: string }, b: { name: string }) => (a.name < b.name ? -1 : 1);
  48. const sortedTypes = Object.values(schema.getTypeMap()).sort(sortByName);
  49. for (const type of sortedTypes) {
  50. if (type.name.substring(0, 2) === '__') {
  51. // ignore internal types
  52. continue;
  53. }
  54. if (isObjectType(type)) {
  55. if (type.name === 'Query') {
  56. for (const field of Object.values(type.getFields()).sort(sortByName)) {
  57. if (field.name === 'temp__') {
  58. continue;
  59. }
  60. queriesOutput += `## ${field.name}\n`;
  61. queriesOutput += renderDescription(field);
  62. queriesOutput += renderFields([field], false) + '\n\n';
  63. }
  64. } else if (type.name === 'Mutation') {
  65. for (const field of Object.values(type.getFields()).sort(sortByName)) {
  66. mutationsOutput += `## ${field.name}\n`;
  67. mutationsOutput += renderDescription(field);
  68. mutationsOutput += renderFields([field], false) + '\n\n';
  69. }
  70. } else {
  71. objectTypesOutput += `## ${type.name}\n\n`;
  72. objectTypesOutput += renderDescription(type);
  73. objectTypesOutput += renderFields(type);
  74. objectTypesOutput += '\n';
  75. }
  76. }
  77. if (isEnumType(type)) {
  78. enumsOutput += `## ${type.name}\n\n`;
  79. enumsOutput += renderDescription(type) + '\n\n';
  80. enumsOutput += '{{% gql-enum-values %}}\n';
  81. for (const value of type.getValues()) {
  82. enumsOutput += value.description ? ` * *// ${value.description.trim()}*\n` : '';
  83. enumsOutput += ` * ${value.name}\n`;
  84. }
  85. enumsOutput += '{{% /gql-enum-values %}}\n';
  86. enumsOutput += '\n';
  87. }
  88. if (isScalarType(type)) {
  89. objectTypesOutput += `## ${type.name}\n\n`;
  90. objectTypesOutput += renderDescription(type);
  91. }
  92. if (isInputObjectType(type)) {
  93. inputTypesOutput += `## ${type.name}\n\n`;
  94. inputTypesOutput += renderDescription(type);
  95. inputTypesOutput += renderFields(type);
  96. inputTypesOutput += '\n';
  97. }
  98. if (isUnionType(type)) {
  99. objectTypesOutput += `## ${type.name}\n\n`;
  100. objectTypesOutput += renderDescription(type);
  101. objectTypesOutput += renderUnion(type);
  102. }
  103. }
  104. fs.writeFileSync(path.join(hugoOutputPath, FileName.QUERY + '.md'), queriesOutput);
  105. fs.writeFileSync(path.join(hugoOutputPath, FileName.MUTATION + '.md'), mutationsOutput);
  106. fs.writeFileSync(path.join(hugoOutputPath, FileName.OBJECT + '.md'), objectTypesOutput);
  107. fs.writeFileSync(path.join(hugoOutputPath, FileName.INPUT + '.md'), inputTypesOutput);
  108. fs.writeFileSync(path.join(hugoOutputPath, FileName.ENUM + '.md'), enumsOutput);
  109. console.log(`Generated 5 GraphQL API docs in ${+new Date() - timeStart}ms`);
  110. }
  111. /**
  112. * Renders the type description if it exists.
  113. */
  114. function renderDescription(type: { description?: string | null }): string {
  115. if (!type.description) {
  116. return '';
  117. }
  118. // Strip any JSDoc tags which may be used to annotate the generated
  119. // TS types.
  120. const stringsToStrip = [/@docsCategory\s+[^\n]+/g, /@description\s+/g];
  121. let result = type.description;
  122. for (const pattern of stringsToStrip) {
  123. result = result.replace(pattern, '');
  124. }
  125. return result + '\n\n';
  126. }
  127. function renderFields(
  128. typeOrFields: (GraphQLObjectType | GraphQLInputObjectType) | Array<GraphQLField<any, any>>,
  129. includeDescription = true,
  130. ): string {
  131. let output = '{{% gql-fields %}}\n';
  132. const fieldsArray: Array<GraphQLField<any, any>> = Array.isArray(typeOrFields)
  133. ? typeOrFields
  134. : Object.values(typeOrFields.getFields());
  135. for (const field of fieldsArray) {
  136. if (includeDescription) {
  137. output += field.description ? `* *// ${field.description.trim()}*\n` : '';
  138. }
  139. output += ` * ${renderFieldSignature(field)}\n`;
  140. }
  141. output += '{{% /gql-fields %}}\n\n';
  142. return output;
  143. }
  144. function renderUnion(type: GraphQLUnionType): string {
  145. const unionTypes = type
  146. .getTypes()
  147. .map(t => renderTypeAsLink(t))
  148. .join(' | ');
  149. let output = '{{% gql-fields %}}\n';
  150. output += `union ${type.name} = ${unionTypes}\n`;
  151. output += '{{% /gql-fields %}}\n\n';
  152. return output;
  153. }
  154. /**
  155. * Renders a field signature including any argument and output type
  156. */
  157. function renderFieldSignature(field: GraphQLField<any, any>): string {
  158. let name = field.name;
  159. if (field.args && field.args.length) {
  160. name += `(${field.args.map(arg => arg.name + ': ' + renderTypeAsLink(arg.type)).join(', ')})`;
  161. }
  162. return `${name}: ${renderTypeAsLink(field.type)}`;
  163. }
  164. /**
  165. * Renders a type as a markdown link.
  166. */
  167. function renderTypeAsLink(type: GraphQLType): string {
  168. const innerType = unwrapType(type);
  169. const fileName = isEnumType(innerType)
  170. ? FileName.ENUM
  171. : isInputObjectType(innerType)
  172. ? FileName.INPUT
  173. : FileName.OBJECT;
  174. const url = `${docsUrl}${fileName}#${innerType.name.toLowerCase()}`;
  175. return type.toString().replace(innerType.name, `[${innerType.name}](${url})`);
  176. }
  177. /**
  178. * Unwraps the inner type from a higher-order type, e.g. [Address!]! => Address
  179. */
  180. function unwrapType(type: GraphQLType): GraphQLNamedType {
  181. if (isNamedType(type)) {
  182. return type;
  183. }
  184. let innerType = type as GraphQLType;
  185. while (!isNamedType(innerType)) {
  186. innerType = innerType.ofType;
  187. }
  188. return innerType;
  189. }
  190. function getTargetApiFromArgs(): TargetApi {
  191. const apiArg = process.argv.find(arg => /--api=(shop|admin)/.test(arg));
  192. if (!apiArg) {
  193. console.error('\nPlease specify which GraphQL API to generate docs for: --api=<shop|admin>\n');
  194. process.exit(1);
  195. return null as never;
  196. }
  197. return apiArg === '--api=shop' ? 'shop' : 'admin';
  198. }