generate-api-docs.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import fs from 'fs';
  2. import {
  3. buildClientSchema,
  4. GraphQLField,
  5. GraphQLInputObjectType,
  6. GraphQLNamedType,
  7. GraphQLObjectType,
  8. GraphQLType,
  9. isEnumType,
  10. isInputObjectType,
  11. isNamedType,
  12. isObjectType,
  13. isScalarType,
  14. } from 'graphql';
  15. import path from 'path';
  16. import { deleteGeneratedDocs, generateFrontMatter } from './docgen-utils';
  17. // tslint:disable:no-console
  18. // The path to the introspection schema json file
  19. const SCHEMA_FILE = path.join(__dirname, '../schema.json');
  20. // The absolute URL to the generated api docs section
  21. const docsUrl = '/docs/graphql-api/';
  22. // The directory in which the markdown files will be saved
  23. const outputPath = path.join(__dirname, '../docs/content/docs/graphql-api');
  24. const enum FileName {
  25. ENUM = 'enums',
  26. INPUT = 'input-types',
  27. MUTATION = 'mutations',
  28. QUERY = 'queries',
  29. OBJECT = 'object-types',
  30. }
  31. const schemaJson = fs.readFileSync(SCHEMA_FILE, 'utf8');
  32. const parsed = JSON.parse(schemaJson);
  33. const schema = buildClientSchema(parsed.data);
  34. deleteGeneratedDocs(outputPath);
  35. generateApiDocs(outputPath);
  36. function generateApiDocs(hugoOutputPath: string) {
  37. const timeStart = +new Date();
  38. let queriesOutput = generateFrontMatter('Queries', 1) + `\n\n# Queries\n\n`;
  39. let mutationsOutput = generateFrontMatter('Mutations', 2) + `\n\n# Mutations\n\n`;
  40. let objectTypesOutput = generateFrontMatter('Types', 3) + `\n\n# Types\n\n`;
  41. let inputTypesOutput = generateFrontMatter('Input Objects', 4) + `\n\n# Input Objects\n\n`;
  42. let enumsOutput = generateFrontMatter('Enums', 5) + `\n\n# Enums\n\n`;
  43. for (const type of Object.values(schema.getTypeMap())) {
  44. if (type.name.substring(0, 2) === '__') {
  45. // ignore internal types
  46. continue;
  47. }
  48. if (isObjectType(type)) {
  49. if (type.name === 'Query') {
  50. for (const field of Object.values(type.getFields())) {
  51. queriesOutput += `## ${field.name}\n`;
  52. queriesOutput += renderDescription(field);
  53. queriesOutput += renderFields([field], false) + '\n\n';
  54. }
  55. } else if (type.name === 'Mutation') {
  56. for (const field of Object.values(type.getFields())) {
  57. mutationsOutput += `## ${field.name}\n`;
  58. mutationsOutput += renderDescription(field);
  59. mutationsOutput += renderFields([field], false) + '\n\n';
  60. }
  61. } else {
  62. objectTypesOutput += `## ${type.name}\n\n`;
  63. objectTypesOutput += renderDescription(type);
  64. objectTypesOutput += renderFields(type);
  65. objectTypesOutput += `\n`;
  66. }
  67. }
  68. if (isEnumType(type)) {
  69. enumsOutput += `## ${type.name}\n\n`;
  70. enumsOutput += renderDescription(type) + '\n\n';
  71. enumsOutput += '{{% gql-enum-values %}}\n';
  72. for (const value of type.getValues()) {
  73. enumsOutput += value.description ? ` * *// ${value.description.trim()}*\n` : '';
  74. enumsOutput += ` * ${value.name}\n`;
  75. }
  76. enumsOutput += '{{% /gql-enum-values %}}\n';
  77. enumsOutput += '\n';
  78. }
  79. if (isScalarType(type)) {
  80. objectTypesOutput += `## ${type.name}\n\n`;
  81. objectTypesOutput += renderDescription(type);
  82. }
  83. if (isInputObjectType(type)) {
  84. inputTypesOutput += `## ${type.name}\n\n`;
  85. inputTypesOutput += renderDescription(type);
  86. inputTypesOutput += renderFields(type);
  87. inputTypesOutput += `\n`;
  88. }
  89. }
  90. fs.writeFileSync(path.join(hugoOutputPath, FileName.QUERY + '.md'), queriesOutput);
  91. fs.writeFileSync(path.join(hugoOutputPath, FileName.MUTATION + '.md'), mutationsOutput);
  92. fs.writeFileSync(path.join(hugoOutputPath, FileName.OBJECT + '.md'), objectTypesOutput);
  93. fs.writeFileSync(path.join(hugoOutputPath, FileName.INPUT + '.md'), inputTypesOutput);
  94. fs.writeFileSync(path.join(hugoOutputPath, FileName.ENUM + '.md'), enumsOutput);
  95. console.log(`Generated 5 GraphQL API docs in ${+new Date() - timeStart}ms`);
  96. }
  97. /**
  98. * Renders the type description if it exists.
  99. */
  100. function renderDescription(type: { description?: string | null }, appendNewlines = true): string {
  101. return type.description ? `${type.description + (appendNewlines ? '\n\n' : '')}` : '';
  102. }
  103. function renderFields(
  104. typeOrFields: (GraphQLObjectType | GraphQLInputObjectType) | Array<GraphQLField<any, any>>,
  105. includeDescription = true,
  106. ): string {
  107. let output = '{{% gql-fields %}}\n';
  108. const fieldsArray: Array<GraphQLField<any, any>> = Array.isArray(typeOrFields)
  109. ? typeOrFields
  110. : Object.values(typeOrFields.getFields());
  111. for (const field of fieldsArray) {
  112. if (includeDescription) {
  113. output += field.description ? `* *// ${field.description.trim()}*\n` : '';
  114. }
  115. output += ` * ${renderFieldSignature(field)}\n`;
  116. }
  117. output += '{{% /gql-fields %}}\n\n';
  118. return output;
  119. }
  120. /**
  121. * Renders a field signature including any argument and output type
  122. */
  123. function renderFieldSignature(field: GraphQLField<any, any>): string {
  124. let name = field.name;
  125. if (field.args && field.args.length) {
  126. name += `(${field.args.map(arg => arg.name + ': ' + renderTypeAsLink(arg.type)).join(', ')})`;
  127. }
  128. return `${name}: ${renderTypeAsLink(field.type)}`;
  129. }
  130. /**
  131. * Renders a type as a markdown link.
  132. */
  133. function renderTypeAsLink(type: GraphQLType): string {
  134. const innerType = unwrapType(type);
  135. const fileName = isEnumType(innerType)
  136. ? FileName.ENUM
  137. : isInputObjectType(innerType)
  138. ? FileName.INPUT
  139. : FileName.OBJECT;
  140. const url = `${docsUrl}${fileName}#${innerType.name.toLowerCase()}`;
  141. return type.toString().replace(innerType.name, `[${innerType.name}](${url})`);
  142. }
  143. /**
  144. * Unwraps the inner type from a higher-order type, e.g. [Address!]! => Address
  145. */
  146. function unwrapType(type: GraphQLType): GraphQLNamedType {
  147. if (isNamedType(type)) {
  148. return type;
  149. }
  150. let innerType = type;
  151. while (!isNamedType(innerType)) {
  152. innerType = innerType.ofType;
  153. }
  154. return innerType;
  155. }