1
0

generate-graphql-docs.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  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 = `/reference/graphql-api/${targetApi}/`;
  26. // The directory in which the markdown files will be saved
  27. const outputPath = path.join(__dirname, `../../docs/docs/reference/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') + '\n\n';
  43. let mutationsOutput = generateFrontMatter('Mutations') + '\n\n';
  44. let objectTypesOutput = generateFrontMatter('Types') + '\n\n';
  45. let inputTypesOutput = generateFrontMatter('Input Objects') + '\n\n';
  46. let enumsOutput = generateFrontMatter('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 += `\n## ${field.name}\n`;
  61. queriesOutput += `<div class="graphql-code-block">\n`;
  62. queriesOutput += renderDescription(field, 'multi', true);
  63. queriesOutput += codeLine(`type ${identifier('Query')} &#123;`, ['top-level']);
  64. queriesOutput += renderFields([field], false);
  65. queriesOutput += codeLine(`&#125;`, ['top-level']);
  66. queriesOutput += `</div>\n`;
  67. }
  68. } else if (type.name === 'Mutation') {
  69. for (const field of Object.values(type.getFields()).sort(sortByName)) {
  70. mutationsOutput += `\n## ${field.name}\n`;
  71. mutationsOutput += `<div class="graphql-code-block">\n`;
  72. mutationsOutput += renderDescription(field, 'multi', true);
  73. mutationsOutput += codeLine(`type ${identifier('Mutation')} &#123;`, ['top-level']);
  74. mutationsOutput += renderFields([field], false);
  75. mutationsOutput += codeLine(`&#125;`, ['top-level']);
  76. mutationsOutput += `</div>\n`;
  77. }
  78. } else {
  79. objectTypesOutput += `\n## ${type.name}\n\n`;
  80. objectTypesOutput += `<div class="graphql-code-block">\n`;
  81. objectTypesOutput += renderDescription(type, 'multi', true);
  82. objectTypesOutput += codeLine(`type ${identifier(type.name)} &#123;`, ['top-level']);
  83. objectTypesOutput += renderFields(type);
  84. objectTypesOutput += codeLine(`&#125;`, ['top-level']);
  85. objectTypesOutput += `</div>\n`;
  86. }
  87. }
  88. if (isEnumType(type)) {
  89. enumsOutput += `\n## ${type.name}\n\n`;
  90. enumsOutput += `<div class="graphql-code-block">\n`;
  91. enumsOutput += renderDescription(type) + '\n';
  92. enumsOutput += codeLine(`enum ${identifier(type.name)} &#123;`, ['top-level']);
  93. for (const value of type.getValues()) {
  94. enumsOutput += value.description ? renderDescription(value.description, 'single') : '';
  95. enumsOutput += codeLine(value.name);
  96. }
  97. enumsOutput += codeLine(`&#125;`, ['top-level']);
  98. enumsOutput += '</div>\n';
  99. }
  100. if (isScalarType(type)) {
  101. objectTypesOutput += `\n## ${type.name}\n\n`;
  102. objectTypesOutput += `<div class="graphql-code-block">\n`;
  103. objectTypesOutput += renderDescription(type, 'multi', true);
  104. objectTypesOutput += codeLine(`scalar ${identifier(type.name)}`, ['top-level']);
  105. objectTypesOutput += '</div>\n';
  106. }
  107. if (isInputObjectType(type)) {
  108. inputTypesOutput += `\n## ${type.name}\n\n`;
  109. inputTypesOutput += `<div class="graphql-code-block">\n`;
  110. inputTypesOutput += renderDescription(type, 'multi', true);
  111. inputTypesOutput += codeLine(`input ${identifier(type.name)} &#123;`, ['top-level']);
  112. inputTypesOutput += renderFields(type);
  113. inputTypesOutput += codeLine(`&#125;`, ['top-level']);
  114. inputTypesOutput += '</div>\n';
  115. }
  116. if (isUnionType(type)) {
  117. objectTypesOutput += `\n## ${type.name}\n\n`;
  118. objectTypesOutput += `<div class="graphql-code-block">\n`;
  119. objectTypesOutput += renderDescription(type);
  120. objectTypesOutput += codeLine(`union ${identifier(type.name)} =`, ['top-level']);
  121. objectTypesOutput += renderUnion(type);
  122. objectTypesOutput += '</div>\n';
  123. }
  124. }
  125. fs.writeFileSync(path.join(hugoOutputPath, FileName.QUERY + '.md'), queriesOutput);
  126. fs.writeFileSync(path.join(hugoOutputPath, FileName.MUTATION + '.md'), mutationsOutput);
  127. fs.writeFileSync(path.join(hugoOutputPath, FileName.OBJECT + '.md'), objectTypesOutput);
  128. fs.writeFileSync(path.join(hugoOutputPath, FileName.INPUT + '.md'), inputTypesOutput);
  129. fs.writeFileSync(path.join(hugoOutputPath, FileName.ENUM + '.md'), enumsOutput);
  130. console.log(`Generated 5 GraphQL API docs in ${+new Date() - timeStart}ms`);
  131. }
  132. function codeLine(content: string, extraClass?: ['top-level' | 'comment'] | undefined): string {
  133. return `<div class="graphql-code-line ${extraClass ? extraClass.join(' ') : ''}">${content}</div>\n`;
  134. }
  135. function identifier(name: string): string {
  136. return `<span class="graphql-code-identifier">${name}</span>`;
  137. }
  138. /**
  139. * Renders the type description if it exists.
  140. */
  141. function renderDescription(
  142. typeOrDescription: { description?: string | null } | string,
  143. mode: 'single' | 'multi' = 'multi',
  144. topLevel = false,
  145. ): string {
  146. let description = '';
  147. if (typeof typeOrDescription === 'string') {
  148. description = typeOrDescription;
  149. } else if (!typeOrDescription.description) {
  150. return '';
  151. } else {
  152. description = typeOrDescription.description;
  153. }
  154. if (description.trim() === '') {
  155. return '';
  156. }
  157. description = description
  158. .replace(/</g, '&lt;')
  159. .replace(/>/g, '&gt;')
  160. .replace(/{/g, '&#123;')
  161. .replace(/}/g, '&#125;');
  162. // Strip any JSDoc tags which may be used to annotate the generated
  163. // TS types.
  164. const stringsToStrip = [/@docsCategory\s+[^\n]+/g, /@description\s+/g];
  165. for (const pattern of stringsToStrip) {
  166. description = description.replace(pattern, '');
  167. }
  168. let result = '';
  169. const extraClass = topLevel ? ['top-level', 'comment'] : (['comment'] as any);
  170. if (mode === 'single') {
  171. result = codeLine(`"""${description}"""`, extraClass);
  172. } else {
  173. result =
  174. codeLine(`"""`, extraClass) +
  175. description
  176. .split('\n')
  177. .map(line => codeLine(`${line}`, extraClass))
  178. .join('\n') +
  179. codeLine(`"""`, extraClass);
  180. }
  181. result = result.replace(/\s`([^`]+)`\s/g, ' <code>$1</code> ');
  182. return result;
  183. }
  184. function renderFields(
  185. typeOrFields: (GraphQLObjectType | GraphQLInputObjectType) | Array<GraphQLField<any, any>>,
  186. includeDescription = true,
  187. ): string {
  188. let output = '';
  189. const fieldsArray: Array<GraphQLField<any, any>> = Array.isArray(typeOrFields)
  190. ? typeOrFields
  191. : Object.values(typeOrFields.getFields());
  192. for (const field of fieldsArray) {
  193. if (includeDescription) {
  194. output += field.description ? renderDescription(field.description) : '';
  195. }
  196. output += `${renderFieldSignature(field)}\n`;
  197. }
  198. output += '\n';
  199. return output;
  200. }
  201. function renderUnion(type: GraphQLUnionType): string {
  202. const unionTypes = type
  203. .getTypes()
  204. .map(t => renderTypeAsLink(t))
  205. .join(' | ');
  206. return codeLine(`${unionTypes}`);
  207. }
  208. /**
  209. * Renders a field signature including any argument and output type
  210. */
  211. function renderFieldSignature(field: GraphQLField<any, any>): string {
  212. let name = field.name;
  213. if (field.args && field.args.length) {
  214. name += `(${field.args.map(arg => arg.name + ': ' + renderTypeAsLink(arg.type)).join(', ')})`;
  215. }
  216. return codeLine(`${name}: ${renderTypeAsLink(field.type)}`);
  217. }
  218. /**
  219. * Renders a type as an anchor link.
  220. */
  221. function renderTypeAsLink(type: GraphQLType): string {
  222. const innerType = unwrapType(type);
  223. const fileName = isEnumType(innerType)
  224. ? FileName.ENUM
  225. : isInputObjectType(innerType)
  226. ? FileName.INPUT
  227. : FileName.OBJECT;
  228. const url = `${docsUrl}${fileName}#${innerType.name.toLowerCase()}`;
  229. return type.toString().replace(innerType.name, `<a href="${url}">${innerType.name}</a>`);
  230. }
  231. /**
  232. * Unwraps the inner type from a higher-order type, e.g. [Address!]! => Address
  233. */
  234. function unwrapType(type: GraphQLType): GraphQLNamedType {
  235. if (isNamedType(type)) {
  236. return type;
  237. }
  238. let innerType = type as GraphQLType;
  239. while (!isNamedType(innerType)) {
  240. innerType = innerType.ofType;
  241. }
  242. return innerType;
  243. }
  244. function getTargetApiFromArgs(): TargetApi {
  245. const apiArg = process.argv.find(arg => /--api=(shop|admin)/.test(arg));
  246. if (!apiArg) {
  247. console.error('\nPlease specify which GraphQL API to generate docs for: --api=<shop|admin>\n');
  248. process.exit(1);
  249. return null as never;
  250. }
  251. return apiArg === '--api=shop' ? 'shop' : 'admin';
  252. }