| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505 |
- import fs from 'fs';
- import {
- buildClientSchema,
- GraphQLEnumType,
- GraphQLField,
- GraphQLInputObjectType,
- GraphQLNamedType,
- GraphQLObjectType,
- GraphQLScalarType,
- GraphQLType,
- GraphQLUnionType,
- isEnumType,
- isInputObjectType,
- isListType,
- isNamedType,
- isNonNullType,
- 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',
- }
- type GraphQLDocType = 'query' | 'mutation' | 'type' | 'input' | 'enum' | 'scalar' | 'union';
- const schemaJson = fs.readFileSync(SCHEMA_FILE, 'utf8');
- const parsed = JSON.parse(schemaJson);
- const schema = buildClientSchema(parsed.data ? parsed.data : parsed);
- // ============================================================================
- // Type Reference Collection Helpers
- // ============================================================================
- /**
- * Collects all referenced type names from a GraphQL type (unwrapping NonNull/List wrappers)
- */
- function collectReferencedTypes(type: GraphQLType): Set<string> {
- const types = new Set<string>();
- let current = type;
- while (!isNamedType(current)) {
- if (isNonNullType(current) || isListType(current)) {
- current = current.ofType;
- }
- }
- if (isNamedType(current)) {
- types.add(current.name);
- }
- return types;
- }
- /**
- * Collects all referenced types from a field (including arguments and return type)
- */
- function collectFieldReferencedTypes(field: GraphQLField<any, any>): Set<string> {
- const types = new Set<string>();
- // Return type
- for (const t of collectReferencedTypes(field.type)) {
- types.add(t);
- }
- // Argument types
- if (field.args) {
- for (const arg of field.args) {
- for (const t of collectReferencedTypes(arg.type)) {
- types.add(t);
- }
- }
- }
- return types;
- }
- /**
- * Collects all referenced types from an object or input type's fields
- */
- function collectTypeReferencedTypes(type: GraphQLObjectType | GraphQLInputObjectType): Set<string> {
- const types = new Set<string>();
- for (const field of Object.values(type.getFields())) {
- for (const t of collectFieldReferencedTypes(field as GraphQLField<any, any>)) {
- types.add(t);
- }
- }
- return types;
- }
- /**
- * Collects all types from a union
- */
- function collectUnionReferencedTypes(type: GraphQLUnionType): Set<string> {
- const types = new Set<string>();
- for (const t of type.getTypes()) {
- types.add(t.name);
- }
- return types;
- }
- /**
- * Builds a map of type names to their documentation URLs
- */
- function buildTypeLinksMap(referencedTypes: Set<string>): Record<string, string> {
- const typeLinks: Record<string, string> = {};
- for (const typeName of referencedTypes) {
- const schemaType = schema.getType(typeName);
- if (!schemaType) continue;
- let fileName: FileName;
- if (isEnumType(schemaType)) {
- fileName = FileName.ENUM;
- } else if (isInputObjectType(schemaType)) {
- fileName = FileName.INPUT;
- } else {
- fileName = FileName.OBJECT;
- }
- typeLinks[typeName] = `${docsUrl}${fileName}#${typeName.toLowerCase()}`;
- }
- return typeLinks;
- }
- // ============================================================================
- // SDL Rendering Helpers (plain GraphQL text, no HTML)
- // ============================================================================
- /**
- * Escapes template string special characters (backticks and ${)
- */
- function escapeTemplateString(str: string): string {
- return str.replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
- }
- /**
- * Strips JSDoc-style tags from descriptions
- */
- function stripJSDocTags(description: string): string {
- const stringsToStrip = [/@docsCategory\s+[^\n]+/g, /@description\s+/g];
- let result = description;
- for (const pattern of stringsToStrip) {
- result = result.replace(pattern, '');
- }
- return result.trim();
- }
- /**
- * Renders a description as SDL triple-quote format
- */
- function renderSDLDescription(description: string | null | undefined, indent = ''): string {
- if (!description || description.trim() === '') {
- return '';
- }
- const cleanDescription = stripJSDocTags(description);
- if (cleanDescription === '') {
- return '';
- }
- const lines = cleanDescription.split('\n');
- if (lines.length === 1) {
- return `${indent}"""${cleanDescription}"""\n`;
- }
- return `${indent}"""\n${lines.map(line => `${indent}${line}`).join('\n')}\n${indent}"""\n`;
- }
- /**
- * Renders a GraphQL type as a string (e.g., [String!]!)
- */
- function renderTypeString(type: GraphQLType): string {
- return type.toString();
- }
- /**
- * Renders a single field as SDL
- */
- function renderFieldSDL(
- field: GraphQLField<any, any>,
- indent = ' ',
- includeDescription = true,
- ): string {
- let result = '';
- if (includeDescription && field.description) {
- result += renderSDLDescription(field.description, indent);
- }
- let fieldLine = `${indent}${field.name}`;
- if (field.args && field.args.length > 0) {
- const args = field.args.map(arg => `${arg.name}: ${renderTypeString(arg.type)}`).join(', ');
- fieldLine += `(${args})`;
- }
- fieldLine += `: ${renderTypeString(field.type)}`;
- result += fieldLine + '\n';
- return result;
- }
- /**
- * Renders an object type as SDL
- */
- function renderObjectTypeSDL(type: GraphQLObjectType): string {
- let result = '';
- if (type.description) {
- result += renderSDLDescription(type.description);
- }
- result += `type ${type.name} {\n`;
- for (const field of Object.values(type.getFields())) {
- result += renderFieldSDL(field);
- }
- result += '}';
- return result;
- }
- /**
- * Renders an input type as SDL
- */
- function renderInputTypeSDL(type: GraphQLInputObjectType): string {
- let result = '';
- if (type.description) {
- result += renderSDLDescription(type.description);
- }
- result += `input ${type.name} {\n`;
- for (const field of Object.values(type.getFields())) {
- let fieldResult = '';
- if (field.description) {
- fieldResult += renderSDLDescription(field.description, ' ');
- }
- fieldResult += ` ${field.name}: ${renderTypeString(field.type)}\n`;
- result += fieldResult;
- }
- result += '}';
- return result;
- }
- /**
- * Renders an enum type as SDL
- */
- function renderEnumTypeSDL(type: GraphQLEnumType): string {
- let result = '';
- if (type.description) {
- result += renderSDLDescription(type.description);
- }
- result += `enum ${type.name} {\n`;
- for (const value of type.getValues()) {
- if (value.description) {
- result += renderSDLDescription(value.description, ' ');
- }
- result += ` ${value.name}\n`;
- }
- result += '}';
- return result;
- }
- /**
- * Renders a scalar type as SDL
- */
- function renderScalarSDL(type: GraphQLScalarType): string {
- let result = '';
- if (type.description) {
- result += renderSDLDescription(type.description);
- }
- result += `scalar ${type.name}`;
- return result;
- }
- /**
- * Renders a union type as SDL
- */
- function renderUnionSDL(type: GraphQLUnionType): string {
- let result = '';
- if (type.description) {
- result += renderSDLDescription(type.description);
- }
- const members = type.getTypes().map(t => t.name).join(' | ');
- result += `union ${type.name} = ${members}`;
- return result;
- }
- /**
- * Renders a query/mutation field wrapped in its parent type as SDL
- * Note: Description is included at the top level, not inside the field
- */
- function renderQueryMutationFieldSDL(field: GraphQLField<any, any>, parentTypeName: 'Query' | 'Mutation'): string {
- let result = '';
- if (field.description) {
- result += renderSDLDescription(field.description);
- }
- result += `type ${parentTypeName} {\n`;
- // Skip description inside the field since it's already at the top level
- result += renderFieldSDL(field, ' ', false);
- result += '}';
- return result;
- }
- // ============================================================================
- // GraphQLDoc Component Generator
- // ============================================================================
- /**
- * Renders a GraphQLDoc component for MDX
- */
- function renderGraphQLDocComponent(options: {
- type: GraphQLDocType;
- typeName: string;
- sdlContent: string;
- typeLinks: Record<string, string>;
- deprecated?: boolean | string;
- }): string {
- const { type, typeName, sdlContent, typeLinks, deprecated } = options;
- const escapedSdl = escapeTemplateString(sdlContent);
- // Build typeLinks prop
- const typeLinksEntries = Object.entries(typeLinks);
- let typeLinksStr = '{}';
- if (typeLinksEntries.length > 0) {
- const entries = typeLinksEntries
- .map(([name, url]) => ` ${name}: '${url}'`)
- .join(',\n');
- typeLinksStr = `{\n${entries},\n }`;
- }
- let result = `<GraphQLDoc\n`;
- result += ` type="${type}"\n`;
- result += ` typeName="${typeName}"\n`;
- result += ` typeLinks={${typeLinksStr}}\n`;
- if (deprecated) {
- if (typeof deprecated === 'string') {
- result += ` deprecated="${escapeTemplateString(deprecated)}"\n`;
- } else {
- result += ` deprecated={true}\n`;
- }
- }
- result += `>\n`;
- result += `{\`${escapedSdl}\`}\n`;
- result += `</GraphQLDoc>`;
- return result;
- }
- deleteGeneratedDocs(outputPath);
- generateGraphqlDocs(outputPath);
- function generateGraphqlDocs(hugoOutputPath: string) {
- const timeStart = +new Date();
- if (!fs.existsSync(hugoOutputPath)) {
- fs.mkdirSync(hugoOutputPath, { recursive: true });
- }
- 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') {
- // Handle Query fields
- for (const field of Object.values(type.getFields()).sort(sortByName)) {
- if (field.name === 'temp__') {
- continue;
- }
- const referencedTypes = collectFieldReferencedTypes(field);
- const typeLinks = buildTypeLinksMap(referencedTypes);
- const sdlContent = renderQueryMutationFieldSDL(field, 'Query');
- const deprecated = field.deprecationReason || undefined;
- queriesOutput += `\n## ${field.name} {#${field.name.toLowerCase()}}\n\n`;
- queriesOutput += renderGraphQLDocComponent({
- type: 'query',
- typeName: field.name,
- sdlContent,
- typeLinks,
- deprecated,
- });
- queriesOutput += '\n';
- }
- } else if (type.name === 'Mutation') {
- // Handle Mutation fields
- for (const field of Object.values(type.getFields()).sort(sortByName)) {
- const referencedTypes = collectFieldReferencedTypes(field);
- const typeLinks = buildTypeLinksMap(referencedTypes);
- const sdlContent = renderQueryMutationFieldSDL(field, 'Mutation');
- const deprecated = field.deprecationReason || undefined;
- mutationsOutput += `\n## ${field.name} {#${field.name.toLowerCase()}}\n\n`;
- mutationsOutput += renderGraphQLDocComponent({
- type: 'mutation',
- typeName: field.name,
- sdlContent,
- typeLinks,
- deprecated,
- });
- mutationsOutput += '\n';
- }
- } else {
- // Handle regular object types
- const referencedTypes = collectTypeReferencedTypes(type);
- const typeLinks = buildTypeLinksMap(referencedTypes);
- const sdlContent = renderObjectTypeSDL(type);
- objectTypesOutput += `\n## ${type.name} {#${type.name.toLowerCase()}}\n\n`;
- objectTypesOutput += renderGraphQLDocComponent({
- type: 'type',
- typeName: type.name,
- sdlContent,
- typeLinks,
- });
- objectTypesOutput += '\n';
- }
- }
- if (isEnumType(type)) {
- const sdlContent = renderEnumTypeSDL(type);
- enumsOutput += `\n## ${type.name} {#${type.name.toLowerCase()}}\n\n`;
- enumsOutput += renderGraphQLDocComponent({
- type: 'enum',
- typeName: type.name,
- sdlContent,
- typeLinks: {},
- });
- enumsOutput += '\n';
- }
- if (isScalarType(type)) {
- const sdlContent = renderScalarSDL(type);
- objectTypesOutput += `\n## ${type.name} {#${type.name.toLowerCase()}}\n\n`;
- objectTypesOutput += renderGraphQLDocComponent({
- type: 'scalar',
- typeName: type.name,
- sdlContent,
- typeLinks: {},
- });
- objectTypesOutput += '\n';
- }
- if (isInputObjectType(type)) {
- const referencedTypes = collectTypeReferencedTypes(type);
- const typeLinks = buildTypeLinksMap(referencedTypes);
- const sdlContent = renderInputTypeSDL(type);
- inputTypesOutput += `\n## ${type.name} {#${type.name.toLowerCase()}}\n\n`;
- inputTypesOutput += renderGraphQLDocComponent({
- type: 'input',
- typeName: type.name,
- sdlContent,
- typeLinks,
- });
- inputTypesOutput += '\n';
- }
- if (isUnionType(type)) {
- const referencedTypes = collectUnionReferencedTypes(type);
- const typeLinks = buildTypeLinksMap(referencedTypes);
- const sdlContent = renderUnionSDL(type);
- objectTypesOutput += `\n## ${type.name} {#${type.name.toLowerCase()}}\n\n`;
- objectTypesOutput += renderGraphQLDocComponent({
- type: 'union',
- typeName: type.name,
- sdlContent,
- typeLinks,
- });
- objectTypesOutput += '\n';
- }
- }
- fs.writeFileSync(path.join(hugoOutputPath, FileName.QUERY + '.mdx'), queriesOutput);
- fs.writeFileSync(path.join(hugoOutputPath, FileName.MUTATION + '.mdx'), mutationsOutput);
- fs.writeFileSync(path.join(hugoOutputPath, FileName.OBJECT + '.mdx'), objectTypesOutput);
- fs.writeFileSync(path.join(hugoOutputPath, FileName.INPUT + '.mdx'), inputTypesOutput);
- fs.writeFileSync(path.join(hugoOutputPath, FileName.ENUM + '.mdx'), enumsOutput);
- console.log(`Generated 5 GraphQL API docs in ${+new Date() - timeStart}ms`);
- }
- 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';
- }
|