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 += `
\n`; queriesOutput += renderDescription(field, 'multi', true); queriesOutput += codeLine(`type ${identifier('Query')} {`, ['top-level']); queriesOutput += renderFields([field], false); queriesOutput += codeLine(`}`, ['top-level']); queriesOutput += `
\n`; } } else if (type.name === 'Mutation') { for (const field of Object.values(type.getFields()).sort(sortByName)) { mutationsOutput += `\n## ${field.name}\n`; mutationsOutput += `
\n`; mutationsOutput += renderDescription(field, 'multi', true); mutationsOutput += codeLine(`type ${identifier('Mutation')} {`, ['top-level']); mutationsOutput += renderFields([field], false); mutationsOutput += codeLine(`}`, ['top-level']); mutationsOutput += `
\n`; } } else { objectTypesOutput += `\n## ${type.name}\n\n`; objectTypesOutput += `
\n`; objectTypesOutput += renderDescription(type, 'multi', true); objectTypesOutput += codeLine(`type ${identifier(type.name)} {`, ['top-level']); objectTypesOutput += renderFields(type); objectTypesOutput += codeLine(`}`, ['top-level']); objectTypesOutput += `
\n`; } } if (isEnumType(type)) { enumsOutput += `\n## ${type.name}\n\n`; enumsOutput += `
\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 += '
\n'; } if (isScalarType(type)) { objectTypesOutput += `\n## ${type.name}\n\n`; objectTypesOutput += `
\n`; objectTypesOutput += renderDescription(type, 'multi', true); objectTypesOutput += codeLine(`scalar ${identifier(type.name)}`, ['top-level']); objectTypesOutput += '
\n'; } if (isInputObjectType(type)) { inputTypesOutput += `\n## ${type.name}\n\n`; inputTypesOutput += `
\n`; inputTypesOutput += renderDescription(type, 'multi', true); inputTypesOutput += codeLine(`input ${identifier(type.name)} {`, ['top-level']); inputTypesOutput += renderFields(type); inputTypesOutput += codeLine(`}`, ['top-level']); inputTypesOutput += '
\n'; } if (isUnionType(type)) { objectTypesOutput += `\n## ${type.name}\n\n`; objectTypesOutput += `
\n`; objectTypesOutput += renderDescription(type); objectTypesOutput += codeLine(`union ${identifier(type.name)} =`, ['top-level']); objectTypesOutput += renderUnion(type); objectTypesOutput += '
\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 `
${content}
\n`; } function identifier(name: string): string { return `${name}`; } /** * 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, '}'); // 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, ' $1 '); return result; } function renderFields( typeOrFields: (GraphQLObjectType | GraphQLInputObjectType) | Array>, includeDescription = true, ): string { let output = ''; const fieldsArray: Array> = 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): 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, `${innerType.name}`); } /** * 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=\n'); process.exit(1); return null as never; } return apiArg === '--api=shop' ? 'shop' : 'admin'; }