UNPKG

@graphql-markdown/core

Version:

GraphQL-Markdown core package for generating Markdown documentation from a GraphQL schema.

405 lines (404 loc) 15.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getRenderer = exports.Renderer = exports.getApiGroupFolder = exports.API_GROUPS = void 0; const node_path_1 = require("node:path"); const graphql_1 = require("@graphql-markdown/graphql"); const utils_1 = require("@graphql-markdown/utils"); const logger_1 = require("@graphql-markdown/logger"); const config_1 = require("./config"); /** * Constant representing the string literal for deprecated entities. * Used for grouping deprecated schema entities when the deprecated option is set to "group". * * @example * ```typescript * // When using the deprecated grouping option * if (isDeprecated(type)) { * const groupName = DEPRECATED; * // Handle deprecated type * } * ``` */ const DEPRECATED = "deprecated"; /** * CSS class names used for styling different categories in the generated documentation. * These are applied to the generated index metafiles for different section types. * * @example * ```typescript * // When generating API section metafiles * await generateIndexMetafile(dirPath, typeCat, { * styleClass: CATEGORY_STYLE_CLASS.API * }); * ``` */ var CATEGORY_STYLE_CLASS; (function (CATEGORY_STYLE_CLASS) { /** * CSS class applied to API sections (operations) */ CATEGORY_STYLE_CLASS["API"] = "graphql-markdown-api-section"; /** * CSS class applied to deprecated entity sections */ CATEGORY_STYLE_CLASS["DEPRECATED"] = "graphql-markdown-deprecated-section"; })(CATEGORY_STYLE_CLASS || (CATEGORY_STYLE_CLASS = {})); /** * Enum defining sidebar position values for ordering categories in the documentation sidebar. * Used to control the relative position of categories in the navigation. * * @example * ```typescript * // Position deprecated entities at the end of the sidebar * await generateIndexMetafile(dirPath, DEPRECATED, { * sidebarPosition: SidebarPosition.LAST * }); * ``` */ var SidebarPosition; (function (SidebarPosition) { /** * Position at the beginning of the sidebar */ SidebarPosition[SidebarPosition["FIRST"] = 1] = "FIRST"; /** * Position at the end of the sidebar */ SidebarPosition[SidebarPosition["LAST"] = 999] = "LAST"; })(SidebarPosition || (SidebarPosition = {})); /** * Default group names for API types and non-API types. * This constant provides the base folder structure for organizing GraphQL schema entities. * Can be overridden via ApiGroupOverrideType in configuration. * * @property operations Folder name for GraphQL operations (queries, mutations, subscriptions) * @property types Folder name for GraphQL type definitions * @useDeclaredType * * @example * ```typescript * // Default structure * const defaultGroups = API_GROUPS; * // { operations: "operations", types: "types" } * * // With custom override * const customGroups = { ...API_GROUPS, operations: "queries-and-mutations" }; * ``` * * @see {@link getApiGroupFolder} For usage with type categorization */ exports.API_GROUPS = { operations: "operations", types: "types", }; /** * Determines the appropriate folder for a GraphQL schema entity based on its type. * * @param type - The GraphQL schema entity to categorize * @param groups - Optional custom group naming configuration * @returns The folder name where the entity should be placed * @useDeclaredType * * @example * ```typescript * // With default groups * const folder = getApiGroupFolder(queryType); // Returns "operations" * * // With custom groups * const folder = getApiGroupFolder(objectType, { operations: "queries" }); // Returns appropriate folder * ``` */ const getApiGroupFolder = (type, groups) => { let folderNames = exports.API_GROUPS; if (groups && typeof groups === "object") { folderNames = { ...exports.API_GROUPS, ...groups }; } return (0, graphql_1.isApiType)(type) ? folderNames.operations : folderNames.types; }; exports.getApiGroupFolder = getApiGroupFolder; /** * Type guard function that checks if the provided options include a specific hierarchy configuration. * * @param options - The renderer options to check * @param hierarchy - The hierarchy type to check for * @returns True if the options contain the specified hierarchy configuration * @useDeclaredType * * @example * ```typescript * if (isHierarchy(options, TypeHierarchy.FLAT)) { * // Handle flat hierarchy structure * } * ``` */ const isHierarchy = (options, hierarchy) => { return (options?.hierarchy?.[hierarchy] && true); }; const isPath = (path) => { return typeof path === "string" && path !== ""; }; /** * Core renderer class responsible for generating documentation files from GraphQL schema entities. * Handles the conversion of schema types to markdown/MDX documentation with proper organization. * @useDeclaredType * @example */ class Renderer { group; outputDir; baseURL; prettify; options; mdxModule; mdxModuleIndexFileSupport; printer; /** * Creates a new Renderer instance. * * @param printer - The printer instance used to convert GraphQL types to markdown * @param outputDir - Directory where documentation will be generated * @param baseURL - Base URL for the documentation * @param group - Optional grouping configuration for schema entities * @param prettify - Whether to format the generated markdown * @param docOptions - Additional documentation options * @param mdxModule - Optional MDX module for enhanced documentation features * @example */ constructor(printer, outputDir, baseURL, group, prettify, docOptions, mdxModule) { this.printer = printer; this.group = group; this.outputDir = outputDir; this.baseURL = baseURL; this.prettify = prettify; this.options = docOptions; this.mdxModule = mdxModule; this.mdxModuleIndexFileSupport = this.hasMDXIndexFileSupport(mdxModule); } /** * Checks if the provided module supports MDX index file generation. * * @param module - The module to check for MDX support * @returns True if the module supports index metafile generation * @useDeclaredType * @example */ hasMDXIndexFileSupport(module = this.mdxModule) { return !!(module && typeof module === "object" && "generateIndexMetafile" in module && typeof module.generateIndexMetafile === "function"); } /** * Generates an index metafile for a category directory if MDX support is available. * * @param dirPath - The directory path where the index should be created * @param category - The category name * @param options - Configuration options for the index * @returns Promise that resolves when the index is generated * @useDeclaredType * * @example * ```typescript * await renderer.generateIndexMetafile('docs/types', 'Types', { * collapsible: true, * collapsed: false * }); * ``` */ async generateIndexMetafile(dirPath, category, options = { collapsible: true, collapsed: true, }) { if (this.mdxModuleIndexFileSupport) { await this.mdxModule.generateIndexMetafile(dirPath, category, { ...options, index: this.options?.index }); } } /** * Generates the directory path and metafiles for a specific schema entity type. * Creates the appropriate directory structure based on configuration options. * * @param type - The schema entity type * @param name - The name of the schema entity * @param rootTypeName - The root type name this entity belongs to * @returns The generated directory path * @useDeclaredType * @example */ async generateCategoryMetafileType(type, name, rootTypeName) { let dirPath = this.outputDir; if (!isPath(dirPath)) { throw new Error("Output directory is empty or not specified"); } if (isHierarchy(this.options, config_1.TypeHierarchy.FLAT)) { return dirPath; } const useApiGroup = isHierarchy(this.options, config_1.TypeHierarchy.API) ? this.options.hierarchy[config_1.TypeHierarchy.API] : !this.options?.hierarchy; if (useApiGroup) { const typeCat = (0, exports.getApiGroupFolder)(type, useApiGroup); dirPath = (0, node_path_1.join)(dirPath, (0, utils_1.slugify)(typeCat)); await this.generateIndexMetafile(dirPath, typeCat, { collapsible: false, collapsed: false, styleClass: CATEGORY_STYLE_CLASS.API, }); } if (this.options?.deprecated === "group" && (0, graphql_1.isDeprecated)(type)) { dirPath = (0, node_path_1.join)(dirPath, (0, utils_1.slugify)(DEPRECATED)); await this.generateIndexMetafile(dirPath, DEPRECATED, { sidebarPosition: SidebarPosition.LAST, styleClass: CATEGORY_STYLE_CLASS.DEPRECATED, }); } if (this.group && rootTypeName in this.group && name in this.group[rootTypeName]) { const rootGroup = this.group[rootTypeName][name] ?? ""; dirPath = (0, node_path_1.join)(dirPath, (0, utils_1.slugify)(rootGroup)); await this.generateIndexMetafile(dirPath, rootGroup); } dirPath = (0, node_path_1.join)(dirPath, (0, utils_1.slugify)(rootTypeName)); await this.generateIndexMetafile(dirPath, rootTypeName); return dirPath; } /** * Renders all types within a root type category (e.g., all Query types). * * @param rootTypeName - The name of the root type (e.g., "Query", "Mutation") * @param type - The type object containing all entities to render * @returns Array of rendered categories or undefined * @useDeclaredType * @example */ async renderRootTypes(rootTypeName, type) { if (typeof type !== "object" || type === null) { return undefined; } const isFlat = isHierarchy(this.options, config_1.TypeHierarchy.FLAT); return Promise.all(Object.keys(type) .map(async (name) => { let dirPath = this.outputDir; if (!isFlat) { dirPath = await this.generateCategoryMetafileType(type[name], name, rootTypeName); } return this.renderTypeEntities(dirPath, name, type[name]); }) .filter((res) => { return typeof res !== "undefined"; })); } /** * Renders documentation for a specific type entity and saves it to a file. * * @param dirPath - The directory path where the file should be saved * @param name - The name of the type entity * @param type - The type entity to render * @returns The category information for the rendered entity or undefined * @useDeclaredType * @example */ async renderTypeEntities(dirPath, name, type) { if (!isPath(dirPath)) { throw new Error("Output directory is empty or not specified"); } const PageRegex = /(?<category>[A-Za-z0-9-]+)[\\/]+(?<pageId>[A-Za-z0-9-]+).mdx?$/; const PageRegexFlat = /(?<pageId>[A-Za-z0-9-]+).mdx?$/; const extension = this.mdxModule ? "mdx" : "md"; const fileName = (0, utils_1.slugify)(name); const filePath = (0, node_path_1.join)((0, node_path_1.normalize)(dirPath), `${fileName}.${extension}`); let content; try { content = this.printer.printType(fileName, type, this.options); if (typeof content !== "string" || content === "") { return undefined; } } catch { (0, logger_1.log)(`An error occurred while processing "${type}"`, logger_1.LogLevel.warn); return undefined; } await (0, utils_1.saveFile)(filePath, content, this.prettify ? utils_1.prettifyMarkdown : undefined); const pagePath = (0, node_path_1.relative)(this.outputDir, filePath); const isFlat = isHierarchy(this.options, config_1.TypeHierarchy.FLAT); const page = isFlat ? PageRegexFlat.exec(pagePath) : PageRegex.exec(pagePath); if (!page?.groups) { (0, logger_1.log)(`An error occurred while processing file ${filePath} for type "${type}"`, logger_1.LogLevel.warn); return undefined; } const slug = isFlat ? page.groups.pageId : utils_1.pathUrl.join(page.groups.category, page.groups.pageId); const category = isFlat ? "schema" : (0, utils_1.startCase)(page.groups.category); return { category, slug, }; } /** * Renders the homepage for the documentation from a template file. * Replaces placeholders in the template with actual values. * * @param homepageLocation - Path to the homepage template file * @returns Promise that resolves when the homepage is rendered * @useDeclaredType * @example */ async renderHomepage(homepageLocation) { if (typeof homepageLocation !== "string") { return; } if (!isPath(this.outputDir)) { throw new Error("Output directory is empty or not specified"); } const homePage = (0, node_path_1.basename)(homepageLocation); const destLocation = (0, node_path_1.join)(this.outputDir, homePage); const slug = utils_1.pathUrl.resolve("/", this.baseURL); try { await (0, utils_1.copyFile)(homepageLocation, destLocation); const template = await (0, utils_1.readFile)(destLocation); const data = template .toString() .replace(/##baseURL##/gm, slug) .replace(/##generated-date-time##/gm, new Date().toLocaleString()); await (0, utils_1.saveFile)(destLocation, data, this.prettify ? utils_1.prettifyMarkdown : undefined); } catch (error) { (0, logger_1.log)(`An error occurred while processing the homepage ${homepageLocation}: ${error}`, logger_1.LogLevel.warn); } } } exports.Renderer = Renderer; /** * Factory function to create and initialize a Renderer instance. * Creates the output directory and returns a configured renderer. * * @param printer - The printer instance to use for rendering types * @param outputDir - The output directory for generated documentation * @param baseURL - The base URL for the documentation * @param group - Optional grouping configuration * @param prettify - Whether to prettify the output markdown * @param docOptions - Additional documentation options * @param mdxModule - Optional MDX module for enhanced features * @returns A configured Renderer instance * @useDeclaredType * * @example * ```typescript * const renderer = await getRenderer( * myPrinter, * './docs', * '/api', * groupConfig, * true, * { force: true, index: true } * ); * ``` */ const getRenderer = async (printer, outputDir, baseURL, group, prettify, docOptions, mdxModule) => { await (0, utils_1.ensureDir)(outputDir, { forceEmpty: docOptions?.force }); return new Renderer(printer, outputDir, baseURL, group, prettify, docOptions, mdxModule); }; exports.getRenderer = getRenderer;