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