| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278 |
- import fs from 'fs';
- import {
- buildClientSchema,
- GraphQLField,
- GraphQLInputObjectType,
- GraphQLNamedType,
- GraphQLObjectType,
- GraphQLType,
- GraphQLUnionType,
- isEnumType,
- isInputObjectType,
- isNamedType,
- isObjectType,
- isScalarType,
- isUnionType,
- } from 'graphql';
- import path from 'path';
- import { deleteGeneratedDocs, generateFrontMatter } from './docgen-utils';
- /* eslint-disable no-console */
- type TargetApi = 'shop' | 'admin';
- const targetApi: TargetApi = getTargetApiFromArgs();
- // The path to the introspection schema json file
- const SCHEMA_FILE = path.join(__dirname, `../../schema-${targetApi}.json`);
- // The absolute URL to the generated api docs section
- const docsUrl = `/reference/graphql-api/${targetApi}/`;
- // The directory in which the markdown files will be saved
- const outputPath = path.join(__dirname, `../../docs/docs/reference/graphql-api/${targetApi}`);
- const enum FileName {
- ENUM = 'enums',
- INPUT = 'input-types',
- MUTATION = 'mutations',
- QUERY = 'queries',
- OBJECT = 'object-types',
- }
- const schemaJson = fs.readFileSync(SCHEMA_FILE, 'utf8');
- const parsed = JSON.parse(schemaJson);
- const schema = buildClientSchema(parsed.data ? parsed.data : parsed);
- deleteGeneratedDocs(outputPath);
- generateGraphqlDocs(outputPath);
- function generateGraphqlDocs(hugoOutputPath: string) {
- const timeStart = +new Date();
- let queriesOutput = generateFrontMatter('Queries') + '\n\n';
- let mutationsOutput = generateFrontMatter('Mutations') + '\n\n';
- let objectTypesOutput = generateFrontMatter('Types') + '\n\n';
- let inputTypesOutput = generateFrontMatter('Input Objects') + '\n\n';
- let enumsOutput = generateFrontMatter('Enums') + '\n\n';
- const sortByName = (a: { name: string }, b: { name: string }) => (a.name < b.name ? -1 : 1);
- const sortedTypes = Object.values(schema.getTypeMap()).sort(sortByName);
- for (const type of sortedTypes) {
- if (type.name.substring(0, 2) === '__') {
- // ignore internal types
- continue;
- }
- if (isObjectType(type)) {
- if (type.name === 'Query') {
- for (const field of Object.values(type.getFields()).sort(sortByName)) {
- if (field.name === 'temp__') {
- continue;
- }
- queriesOutput += `\n## ${field.name}\n`;
- queriesOutput += `<div class="graphql-code-block">\n`;
- queriesOutput += renderDescription(field, 'multi', true);
- queriesOutput += codeLine(`type ${identifier('Query')} {`, ['top-level']);
- queriesOutput += renderFields([field], false);
- queriesOutput += codeLine(`}`, ['top-level']);
- queriesOutput += `</div>\n`;
- }
- } else if (type.name === 'Mutation') {
- for (const field of Object.values(type.getFields()).sort(sortByName)) {
- mutationsOutput += `\n## ${field.name}\n`;
- mutationsOutput += `<div class="graphql-code-block">\n`;
- mutationsOutput += renderDescription(field, 'multi', true);
- mutationsOutput += codeLine(`type ${identifier('Mutation')} {`, ['top-level']);
- mutationsOutput += renderFields([field], false);
- mutationsOutput += codeLine(`}`, ['top-level']);
- mutationsOutput += `</div>\n`;
- }
- } else {
- objectTypesOutput += `\n## ${type.name}\n\n`;
- objectTypesOutput += `<div class="graphql-code-block">\n`;
- objectTypesOutput += renderDescription(type, 'multi', true);
- objectTypesOutput += codeLine(`type ${identifier(type.name)} {`, ['top-level']);
- objectTypesOutput += renderFields(type);
- objectTypesOutput += codeLine(`}`, ['top-level']);
- objectTypesOutput += `</div>\n`;
- }
- }
- if (isEnumType(type)) {
- enumsOutput += `\n## ${type.name}\n\n`;
- enumsOutput += `<div class="graphql-code-block">\n`;
- enumsOutput += renderDescription(type) + '\n';
- enumsOutput += codeLine(`enum ${identifier(type.name)} {`, ['top-level']);
- for (const value of type.getValues()) {
- enumsOutput += value.description ? renderDescription(value.description, 'single') : '';
- enumsOutput += codeLine(value.name);
- }
- enumsOutput += codeLine(`}`, ['top-level']);
- enumsOutput += '</div>\n';
- }
- if (isScalarType(type)) {
- objectTypesOutput += `\n## ${type.name}\n\n`;
- objectTypesOutput += `<div class="graphql-code-block">\n`;
- objectTypesOutput += renderDescription(type, 'multi', true);
- objectTypesOutput += codeLine(`scalar ${identifier(type.name)}`, ['top-level']);
- objectTypesOutput += '</div>\n';
- }
- if (isInputObjectType(type)) {
- inputTypesOutput += `\n## ${type.name}\n\n`;
- inputTypesOutput += `<div class="graphql-code-block">\n`;
- inputTypesOutput += renderDescription(type, 'multi', true);
- inputTypesOutput += codeLine(`input ${identifier(type.name)} {`, ['top-level']);
- inputTypesOutput += renderFields(type);
- inputTypesOutput += codeLine(`}`, ['top-level']);
- inputTypesOutput += '</div>\n';
- }
- if (isUnionType(type)) {
- objectTypesOutput += `\n## ${type.name}\n\n`;
- objectTypesOutput += `<div class="graphql-code-block">\n`;
- objectTypesOutput += renderDescription(type);
- objectTypesOutput += codeLine(`union ${identifier(type.name)} =`, ['top-level']);
- objectTypesOutput += renderUnion(type);
- objectTypesOutput += '</div>\n';
- }
- }
- fs.writeFileSync(path.join(hugoOutputPath, FileName.QUERY + '.md'), queriesOutput);
- fs.writeFileSync(path.join(hugoOutputPath, FileName.MUTATION + '.md'), mutationsOutput);
- fs.writeFileSync(path.join(hugoOutputPath, FileName.OBJECT + '.md'), objectTypesOutput);
- fs.writeFileSync(path.join(hugoOutputPath, FileName.INPUT + '.md'), inputTypesOutput);
- fs.writeFileSync(path.join(hugoOutputPath, FileName.ENUM + '.md'), enumsOutput);
- console.log(`Generated 5 GraphQL API docs in ${+new Date() - timeStart}ms`);
- }
- function codeLine(content: string, extraClass?: ['top-level' | 'comment'] | undefined): string {
- return `<div class="graphql-code-line ${extraClass ? extraClass.join(' ') : ''}">${content}</div>\n`;
- }
- function identifier(name: string): string {
- return `<span class="graphql-code-identifier">${name}</span>`;
- }
- /**
- * Renders the type description if it exists.
- */
- function renderDescription(
- typeOrDescription: { description?: string | null } | string,
- mode: 'single' | 'multi' = 'multi',
- topLevel = false,
- ): string {
- let description = '';
- if (typeof typeOrDescription === 'string') {
- description = typeOrDescription;
- } else if (!typeOrDescription.description) {
- return '';
- } else {
- description = typeOrDescription.description;
- }
- if (description.trim() === '') {
- return '';
- }
- description = description
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/{/g, '{')
- .replace(/}/g, '}');
- // Strip any JSDoc tags which may be used to annotate the generated
- // TS types.
- const stringsToStrip = [/@docsCategory\s+[^\n]+/g, /@description\s+/g];
- for (const pattern of stringsToStrip) {
- description = description.replace(pattern, '');
- }
- let result = '';
- const extraClass = topLevel ? ['top-level', 'comment'] : (['comment'] as any);
- if (mode === 'single') {
- result = codeLine(`"""${description}"""`, extraClass);
- } else {
- result =
- codeLine(`"""`, extraClass) +
- description
- .split('\n')
- .map(line => codeLine(`${line}`, extraClass))
- .join('\n') +
- codeLine(`"""`, extraClass);
- }
- result = result.replace(/\s`([^`]+)`\s/g, ' <code>$1</code> ');
- return result;
- }
- function renderFields(
- typeOrFields: (GraphQLObjectType | GraphQLInputObjectType) | Array<GraphQLField<any, any>>,
- includeDescription = true,
- ): string {
- let output = '';
- const fieldsArray: Array<GraphQLField<any, any>> = Array.isArray(typeOrFields)
- ? typeOrFields
- : Object.values(typeOrFields.getFields());
- for (const field of fieldsArray) {
- if (includeDescription) {
- output += field.description ? renderDescription(field.description) : '';
- }
- output += `${renderFieldSignature(field)}\n`;
- }
- output += '\n';
- return output;
- }
- function renderUnion(type: GraphQLUnionType): string {
- const unionTypes = type
- .getTypes()
- .map(t => renderTypeAsLink(t))
- .join(' | ');
- return codeLine(`${unionTypes}`);
- }
- /**
- * Renders a field signature including any argument and output type
- */
- function renderFieldSignature(field: GraphQLField<any, any>): string {
- let name = field.name;
- if (field.args && field.args.length) {
- name += `(${field.args.map(arg => arg.name + ': ' + renderTypeAsLink(arg.type)).join(', ')})`;
- }
- return codeLine(`${name}: ${renderTypeAsLink(field.type)}`);
- }
- /**
- * Renders a type as an anchor link.
- */
- function renderTypeAsLink(type: GraphQLType): string {
- const innerType = unwrapType(type);
- const fileName = isEnumType(innerType)
- ? FileName.ENUM
- : isInputObjectType(innerType)
- ? FileName.INPUT
- : FileName.OBJECT;
- const url = `${docsUrl}${fileName}#${innerType.name.toLowerCase()}`;
- return type.toString().replace(innerType.name, `<a href="${url}">${innerType.name}</a>`);
- }
- /**
- * Unwraps the inner type from a higher-order type, e.g. [Address!]! => Address
- */
- function unwrapType(type: GraphQLType): GraphQLNamedType {
- if (isNamedType(type)) {
- return type;
- }
- let innerType = type as GraphQLType;
- while (!isNamedType(innerType)) {
- innerType = innerType.ofType;
- }
- return innerType;
- }
- function getTargetApiFromArgs(): TargetApi {
- const apiArg = process.argv.find(arg => /--api=(shop|admin)/.test(arg));
- if (!apiArg) {
- console.error('\nPlease specify which GraphQL API to generate docs for: --api=<shop|admin>\n');
- process.exit(1);
- return null as never;
- }
- return apiArg === '--api=shop' ? 'shop' : 'admin';
- }
|