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 `\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';
}