UNPKG

@graphql-markdown/core

Version:

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

389 lines (388 loc) 16.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.generateDocFromSchema = exports.resolveSkipAndOnlyDirectives = exports.checkSchemaDifferences = exports.loadGraphqlSchema = exports.getFormatterFromMDXModule = exports.getMDXModuleProperty = exports.loadMDXModule = void 0; const logger_1 = __importStar(require("@graphql-markdown/logger")); const graphql_1 = require("@graphql-markdown/graphql"); const utils_1 = require("@graphql-markdown/utils"); const node_path_1 = require("node:path"); const config_1 = require("./config"); const diff_1 = require("./diff"); const printer_1 = require("./printer"); const renderer_1 = require("./renderer"); const event_emitter_1 = require("./event-emitter"); const events_1 = require("./events"); const event_handlers_1 = require("./event-handlers"); /** * Supported file extensions for generated documentation files. * * @remarks * - MDX: MDX file extension (.mdx) for React component-enabled markdown * - MD: Standard markdown file extension (.md) */ const FILE_EXTENSION = { MDX: ".mdx", MD: ".md", }; /** * Constant representing nanoseconds per second. * * This constant is used for high-precision time measurements and conversions * between seconds and nanoseconds. * */ const NS_PER_SEC = 1e9; /** * The number of decimal places to display when reporting times in seconds. * */ const SEC_DECIMALS = 3; /** * Asynchronously loads an MDX module dynamically. * * @param mdxParser - The MDX parser package name or path to import. Can be null or undefined. * @returns A promise that resolves to the imported module, or undefined if: * - The mdxParser parameter is null or undefined * - An error occurs during import (logs a warning and returns undefined) * * @internal */ const loadMDXModule = async (mdxParser) => { return mdxParser !== undefined && mdxParser !== null ? import(mdxParser).catch((error) => { const errorMessage = error instanceof Error ? error.message : String(error); (0, logger_1.log)(`An error occurred while loading MDX formatter "${mdxParser}": ${errorMessage}`, logger_1.LogLevel.warn); return undefined; }) : undefined; }; exports.loadMDXModule = loadMDXModule; /** * List of formatter function names that can be exported individually from an MDX module. */ const FORMATTER_FUNCTION_NAMES = [ "formatMDXBadge", "formatMDXAdmonition", "formatMDXBullet", "formatMDXDetails", "formatMDXFrontmatter", "formatMDXLink", "formatMDXNameEntity", "formatMDXSpecifiedByLink", ]; /** * Gets a property from an MDX module, checking both the module directly and `module.default`. * * This handles the differences between ESM and CommonJS module exports when dynamically importing. * * @param mdxModule - The loaded MDX module * @param propertyName - The name of the property to extract * @returns The property value if found, otherwise undefined * * @internal */ const getMDXModuleProperty = (mdxModule, propertyName) => { if (!mdxModule || typeof mdxModule !== "object") { return undefined; } const module = mdxModule; // Check for property directly on the module if (propertyName in module) { return module[propertyName]; } // Check for property on module.default (ESM default export) const defaultExport = module.default; if (defaultExport && propertyName in defaultExport) { return defaultExport[propertyName]; } return undefined; }; exports.getMDXModuleProperty = getMDXModuleProperty; /** * Extracts a formatter from an MDX module. * * Checks for formatter functions in the following order: * 1. A `createMDXFormatter` factory function (for internal/advanced usage) * 2. Individual exported formatter functions (e.g., `formatMDXBadge`, `formatMDXAdmonition`) * * @param mdxModule - The loaded MDX module that may contain formatter exports * @param meta - Optional metadata to pass to the formatter factory * @returns A partial Formatter with the found functions, or undefined if none found * * @internal */ const getFormatterFromMDXModule = (mdxModule, meta) => { if (!mdxModule || typeof mdxModule !== "object") { return undefined; } const module = mdxModule; // Check for createMDXFormatter on the module directly (internal/advanced usage) if ("createMDXFormatter" in module && typeof module.createMDXFormatter === "function") { return module.createMDXFormatter(meta); } // Check for createMDXFormatter on module.default (ESM default export) const defaultExport = module.default; if (defaultExport && "createMDXFormatter" in defaultExport && typeof defaultExport.createMDXFormatter === "function") { return defaultExport.createMDXFormatter(meta); } // Check for individual formatter functions const formatter = {}; let hasFormatter = false; for (const funcName of FORMATTER_FUNCTION_NAMES) { if (funcName in module && typeof module[funcName] === "function") { formatter[funcName] = module[funcName]; hasFormatter = true; } else if (defaultExport && funcName in defaultExport && typeof defaultExport[funcName] === "function") { formatter[funcName] = defaultExport[funcName]; hasFormatter = true; } } return hasFormatter ? formatter : undefined; }; exports.getFormatterFromMDXModule = getFormatterFromMDXModule; /** * Loads a GraphQL schema from the specified location using configured document loaders. * * @param schemaLocation - The location/path of the GraphQL schema to load (e.g., file path, URL, or glob pattern). * @param loadersList - Optional loader configuration for customizing how the schema is loaded. * * @returns A promise that resolves to the loaded GraphQL schema, or undefined if: * - The loaders cannot be initialized * - An error occurs during schema loading * * @internal */ const loadGraphqlSchema = async (schemaLocation, loadersList) => { const loaders = await (0, graphql_1.getDocumentLoaders)(loadersList); if (!loaders) { (0, logger_1.log)(`An error occurred while loading GraphQL loader.\nCheck your dependencies and configuration.`, "error"); return undefined; } return (0, graphql_1.loadSchema)(schemaLocation, loaders); }; exports.loadGraphqlSchema = loadGraphqlSchema; /** * Checks if there are differences in the GraphQL schema compared to a previous version. * * @param schema - The GraphQL schema to check for differences. * @param schemaLocation - The location/path of the schema file for logging purposes. * @param diffMethod - The method to use for detecting differences. If set to `NONE`, changes detection is skipped. * @param tmpDir - The temporary directory path used for storing and comparing schema versions. * * @returns A promise that resolves to `true` if changes are detected or if diff method is `NONE`, * or `false` if no changes are detected. * * @remarks * When no changes are detected, a log message is generated indicating that the schema is unchanged. */ const checkSchemaDifferences = async (schema, schemaLocation, diffMethod, tmpDir) => { let changed = true; if (diffMethod !== config_1.DiffMethod.NONE) { changed = await (0, diff_1.hasChanges)(schema, tmpDir, diffMethod); if (!changed) { (0, logger_1.log)(`No changes detected in schema "${(0, utils_1.toString)(schemaLocation)}".`); } } return changed; }; exports.checkSchemaDifferences = checkSchemaDifferences; /** * Resolves and retrieves GraphQL directive objects from the schema based on their names. * * Takes two lists of directive names (for "only" and "skip" documentation directives), * looks them up in the provided GraphQL schema, and returns the resolved directive objects. * * @param onlyDocDirective - A directive name or array of directive names for "only" documentation filtering * @param skipDocDirective - A directive name or array of directive names for "skip" documentation filtering * @param schema - The GraphQL schema to resolve directives from * * @returns A tuple containing two arrays: the first with resolved "only" directives, * the second with resolved "skip" directives. Only defined directives are included. */ const resolveSkipAndOnlyDirectives = (onlyDocDirective, skipDocDirective, schema) => { return [onlyDocDirective, skipDocDirective].map((directive) => { // Normalize to array and filter out null/undefined let directiveList; if (Array.isArray(directive)) { directiveList = directive; } else if (directive) { directiveList = [directive]; } else { directiveList = []; } return directiveList .map((name) => { return schema.getDirective(name); }) .filter((directive) => { return directive !== undefined; }); }); }; exports.resolveSkipAndOnlyDirectives = resolveSkipAndOnlyDirectives; /** * Main entry point for generating Markdown documentation from a GraphQL schema. * * This function coordinates the entire documentation generation process: * - Loads and validates the schema * - Checks for schema changes if diffing is enabled * - Processes directives and groups * - Initializes printers and renderers * - Generates markdown files * * @param options - Complete configuration for the documentation generation * @returns Promise that resolves when documentation is fully generated */ const generateDocFromSchema = async ({ baseURL, customDirective, diffMethod, docOptions, force, groupByDirective, homepageLocation, linkRoot, loaders: loadersList, loggerModule, formatter, metatags, onlyDocDirective, outputDir, prettify, printTypeOptions, schemaLocation, skipDocDirective, tmpDir, }) => { const start = process.hrtime.bigint(); await (0, logger_1.default)(loggerModule); const mdxModule = await (0, exports.loadMDXModule)(formatter); // Register MDX lifecycle event handlers if mdxModule loaded successfully // This must be called BEFORE getEvents() because it resets the singleton (0, event_handlers_1.registerMDXEventHandlers)(mdxModule); // Get events AFTER registration (registerMDXEventHandlers resets and recreates the singleton) const events = (0, event_emitter_1.getEvents)(); await events.emitAsync(events_1.SchemaEvents.BEFORE_LOAD, new events_1.SchemaEvent({ schemaLocation: schemaLocation, })); const schema = await (0, exports.loadGraphqlSchema)(schemaLocation, loadersList); await events.emitAsync(events_1.SchemaEvents.AFTER_LOAD, new events_1.SchemaEvent({ schemaLocation: schemaLocation, schema, })); if (!schema) { (0, logger_1.log)(`Failed to load GraphQL schema from location "${(0, utils_1.toString)(schemaLocation)}".`, "error"); return; } await events.emitAsync(events_1.DiffEvents.BEFORE_CHECK, new events_1.DiffCheckEvent({ schema, outputDir: tmpDir, })); const schemaHasChanges = await (0, exports.checkSchemaDifferences)(schema, schemaLocation, diffMethod, tmpDir); await events.emitAsync(events_1.DiffEvents.AFTER_CHECK, new events_1.DiffCheckEvent({ schemaHasChanges })); const [onlyDocDirectives, skipDocDirectives] = (0, exports.resolveSkipAndOnlyDirectives)(onlyDocDirective, skipDocDirective, schema); events.emit(events_1.SchemaEvents.BEFORE_MAP, new events_1.SchemaEvent({ schema })); const rootTypes = (0, graphql_1.getSchemaMap)(schema); events.emit(events_1.SchemaEvents.AFTER_MAP, new events_1.SchemaEvent({ schema, rootTypes })); const customDirectives = (0, graphql_1.getCustomDirectives)(rootTypes, customDirective); const groups = (0, graphql_1.getGroups)(rootTypes, groupByDirective); // Extract formatter from the MDX module (for direct function calls) const mdxFormatter = (0, exports.getFormatterFromMDXModule)(mdxModule, docOptions); // Extract mdxDeclaration from the MDX module (if available) const mdxDeclaration = (0, exports.getMDXModuleProperty)(mdxModule, "mdxDeclaration"); const printer = await (0, printer_1.getPrinter)( // config mandatory { baseURL, linkRoot, schema, }, // options { customDirectives, groups, meta: { generatorFrameworkName: docOptions?.generatorFrameworkName, generatorFrameworkVersion: docOptions?.generatorFrameworkVersion, }, metatags, onlyDocDirectives, printTypeOptions, skipDocDirectives, sectionHeaderId: docOptions?.sectionHeaderId, }, mdxFormatter, mdxDeclaration, // Pass event emitter to enable print events events); // allow mdxModule to specify custom extension const mdxExtensionFromModule = (0, exports.getMDXModuleProperty)(mdxModule, "mdxExtension"); let mdxExtension; if (mdxExtensionFromModule) { mdxExtension = mdxExtensionFromModule; } else if (mdxModule) { mdxExtension = FILE_EXTENSION.MDX; } else { mdxExtension = FILE_EXTENSION.MD; } const renderer = await (0, renderer_1.getRenderer)(printer, outputDir, baseURL, groups, prettify, { ...docOptions, deprecated: printTypeOptions?.deprecated, force, hierarchy: printTypeOptions?.hierarchy, }, mdxExtension); // Pre-collect all categories before rendering to ensure consistent positions renderer.preCollectCategories(Object.keys(rootTypes)); const beforeRenderRootTypesEvent = new events_1.RenderRootTypesEvent({ rootTypes, }); await events.emitAsync(events_1.RenderRootTypesEvents.BEFORE_RENDER, beforeRenderRootTypesEvent); const pages = await Promise.all(Object.keys(rootTypes).map(async (name) => { const typeName = name; return renderer.renderRootTypes(typeName, rootTypes[typeName]); })); const afterRenderRootTypesEvent = new events_1.RenderRootTypesEvent({ rootTypes, }); await events.emitAsync(events_1.RenderRootTypesEvents.AFTER_RENDER, afterRenderRootTypesEvent); const beforeRenderHomepageEvent = new events_1.RenderHomepageEvent({ outputDir, }); await events.emitAsync(events_1.RenderHomepageEvents.BEFORE_RENDER, beforeRenderHomepageEvent); await renderer.renderHomepage(homepageLocation); const afterRenderHomepageEvent = new events_1.RenderHomepageEvent({ outputDir, }); await events.emitAsync(events_1.RenderHomepageEvents.AFTER_RENDER, afterRenderHomepageEvent); await events.emitAsync(events_1.RenderFilesEvents.AFTER_RENDER, new events_1.RenderFilesEvent({ baseURL, outputDir, rootDir: (0, node_path_1.dirname)(outputDir), pages, })); const duration = (Number(process.hrtime.bigint() - start) / NS_PER_SEC).toFixed(SEC_DECIMALS); (0, logger_1.log)(`Documentation successfully generated in "${outputDir}" with base URL "${baseURL}".`, "success"); (0, logger_1.log)(`${pages.flat().length} pages generated in ${duration}s from schema "${(0, utils_1.toString)(schemaLocation)}".`); }; exports.generateDocFromSchema = generateDocFromSchema;