@graphql-markdown/core
Version:
GraphQL-Markdown core package for generating Markdown documentation from a GraphQL schema.
405 lines (404 loc) • 15.5 kB
JavaScript
"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;