@graphql-markdown/core
Version:
GraphQL-Markdown core package for generating Markdown documentation from a GraphQL schema.
623 lines (622 loc) • 25.5 kB
JavaScript
/**
* Configuration management for GraphQL Markdown.
*
* This module handles all aspects of configuration including:
* - Loading and merging configuration from multiple sources
* - Validating configuration values
* - Providing defaults for missing options
* - Processing special configuration options (directives, deprecated items, etc)
*
* The configuration follows this precedence (highest to lowest):
* 1. CLI arguments
* 2. Config file options
* 3. GraphQL Config options
* 4. Default values
*
* @packageDocumentation
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildConfig = exports.parseHomepageOption = exports.parseGroupByOption = exports.getPrintTypeOptions = exports.parseDeprecatedPrintTypeOptions = exports.getTypeHierarchyOption = exports.getDocOptions = exports.parseDeprecatedDocOptions = exports.getDiffMethod = exports.getCustomDirectives = exports.getVisibilityDirectives = exports.getSkipDocDirectives = exports.getOnlyDocDirectives = exports.getDocDirective = exports.DEFAULT_OPTIONS = exports.DEFAULT_HIERARCHY = exports.ASSET_HOMEPAGE_LOCATION = exports.PACKAGE_NAME = exports.DOCS_URL = exports.DeprecatedOption = exports.DiffMethod = exports.TypeHierarchy = void 0;
const node_path_1 = require("node:path");
const node_os_1 = require("node:os");
const deepmerge_1 = require("@fastify/deepmerge");
const graphql_config_1 = require("./graphql-config");
/**
* Type hierarchy options for organizing schema documentation.
*
* - API: Groups types by their role in the API (Query, Mutation, etc.)
* - ENTITY: Groups types by their entity relationships
* - FLAT: No grouping, all types in a flat structure
*
* @public
* @example
* ```typescript
* const hierarchy = TypeHierarchy.API;
* ```
*/
var TypeHierarchy;
(function (TypeHierarchy) {
TypeHierarchy["API"] = "api";
TypeHierarchy["ENTITY"] = "entity";
TypeHierarchy["FLAT"] = "flat";
})(TypeHierarchy || (exports.TypeHierarchy = TypeHierarchy = {}));
/**
* Diff methods used to determine how schema changes are processed.
*
* - NONE: No diffing is performed
* - FORCE: Force regeneration of documentation regardless of schema changes
*
* @public
* @example
* ```typescript
* const diffMethod = DiffMethod.FORCE;
* ```
*/
var DiffMethod;
(function (DiffMethod) {
DiffMethod["NONE"] = "NONE";
DiffMethod["FORCE"] = "FORCE";
})(DiffMethod || (exports.DiffMethod = DiffMethod = {}));
/**
* Options for handling deprecated items in the schema.
*
* - DEFAULT: Show deprecated items normally
* - GROUP: Group deprecated items separately
* - SKIP: Exclude deprecated items from documentation
*
* @public
* @example
* ```typescript
* const deprecatedHandling = DeprecatedOption.GROUP;
* ```
*/
var DeprecatedOption;
(function (DeprecatedOption) {
DeprecatedOption["DEFAULT"] = "default";
DeprecatedOption["GROUP"] = "group";
DeprecatedOption["SKIP"] = "skip";
})(DeprecatedOption || (exports.DeprecatedOption = DeprecatedOption = {}));
/**
* Documentation website URL for reference in error messages and help text.
* @public
*/
exports.DOCS_URL = "https://graphql-markdown.dev/docs";
/**
* Default package name used for temporary directory creation and identification.
* @public
*/
exports.PACKAGE_NAME = "@graphql-markdown/docusaurus";
/**
* Location of the default homepage template.
* @public
*/
exports.ASSET_HOMEPAGE_LOCATION = (0, node_path_1.join)(__dirname, "..", "assets", "generated.md");
/**
* Default hierarchy configuration using the API hierarchy type.
* @public
*/
exports.DEFAULT_HIERARCHY = { [TypeHierarchy.API]: {} };
/**
* Default configuration options used when no user options are provided.
* These values serve as fallbacks for any missing configuration.
*
* @public
* @see {@link Options} for the complete configuration interface
*/
exports.DEFAULT_OPTIONS = {
id: "default",
baseURL: "schema",
customDirective: undefined,
diffMethod: DiffMethod.NONE,
docOptions: {
frontMatter: {},
index: false,
},
force: false,
groupByDirective: undefined,
homepage: exports.ASSET_HOMEPAGE_LOCATION,
linkRoot: "/",
loaders: undefined,
metatags: [],
pretty: false,
printer: "@graphql-markdown/printer-legacy",
printTypeOptions: {
codeSection: true,
deprecated: DeprecatedOption.DEFAULT,
exampleSection: false,
parentTypePrefix: true,
relatedTypeSection: true,
typeBadges: true,
},
rootPath: "./docs",
schema: "./schema.graphql",
tmpDir: (0, node_path_1.join)((0, node_os_1.tmpdir)(), exports.PACKAGE_NAME),
skipDocDirective: [],
onlyDocDirective: [],
};
/**
* Retrieves a directive name from a string by parsing and validating the format.
* Directive names should be prefixed with '\@' (e.g., '\@example').
*
* @param name - The directive name as a string, which should follow the format '\@directiveName'
* @returns The validated directive name without the '\@' prefix
* @throws Error if the directive name format is invalid
* @example
* ```typescript
* const directive = getDocDirective("@example");
* console.log(directive); // "example"
*
* // Invalid - will throw an error
* getDocDirective("example"); // Error: Invalid "example"
* ```
*/
const getDocDirective = (name) => {
const OPTION_REGEX = /^@(?<directive>\w+)$/;
if (typeof name !== "string" || !OPTION_REGEX.test(name)) {
throw new Error(`Invalid "${name}"`);
}
const { groups: { directive }, } = OPTION_REGEX.exec(name);
return directive;
};
exports.getDocDirective = getDocDirective;
/**
* Retrieves the list of "only" directives from CLI and config options.
* These directives specify which schema elements should be included in the documentation.
*
* @param cliOpts - CLI options containing "only" directives
* @param configFileOpts - Config file options containing "onlyDocDirective"
* @returns An array of validated "only" directives (without '\@' prefix)
* @example
* ```typescript
* const cliOptions = { only: ["@example", "@internal"] };
* const configOptions = { onlyDocDirective: ["@auth"] };
*
* const onlyDirectives = getOnlyDocDirectives(cliOptions, configOptions);
* console.log(onlyDirectives); // ["example", "internal", "auth"]
* ```
* @see {@link getDocDirective} for directive name validation
*/
const getOnlyDocDirectives = (cliOpts, configFileOpts) => {
const directiveList = [].concat((cliOpts?.only ?? []), (configFileOpts?.onlyDocDirective ?? []));
const onlyDirectives = directiveList.map((directiveName) => {
return (0, exports.getDocDirective)(directiveName);
});
return onlyDirectives;
};
exports.getOnlyDocDirectives = getOnlyDocDirectives;
/**
* Retrieves the list of "skip" directives from CLI and config options.
* These directives specify which schema elements should be excluded from the documentation.
* Additionally, if deprecated handling is set to SKIP, adds the "deprecated" directive.
*
* @param cliOpts - CLI options containing "skip" directives
* @param configFileOpts - Config file options containing "skipDocDirective" and potentially "printTypeOptions.deprecated"
* @returns An array of validated "skip" directives (without '\@' prefix)
* @example
* ```typescript
* const cliOptions = { skip: ["@internal"], deprecated: "skip" };
* const configOptions = { skipDocDirective: ["@auth"] };
*
* const skipDirectives = getSkipDocDirectives(cliOptions, configOptions);
* console.log(skipDirectives); // ["internal", "auth", "deprecated"]
* ```
* @see {@link getDocDirective} for directive name validation
* @see {@link DeprecatedOption} for deprecated handling options
*/
const getSkipDocDirectives = (cliOpts, configFileOpts) => {
const directiveList = [].concat((cliOpts?.skip ?? []), (configFileOpts?.skipDocDirective ?? []));
const skipDirectives = directiveList.map((directiveName) => {
return (0, exports.getDocDirective)(directiveName);
});
if ((configFileOpts &&
configFileOpts.printTypeOptions?.deprecated === DeprecatedOption.SKIP) ||
(cliOpts && cliOpts.deprecated === DeprecatedOption.SKIP)) {
skipDirectives.push("deprecated");
}
return skipDirectives;
};
exports.getSkipDocDirectives = getSkipDocDirectives;
/**
* Combines and validates visibility directives (only and skip) from both CLI and config sources.
* Ensures that no directive appears in both "only" and "skip" lists simultaneously.
*
* @param cliOpts - CLI options containing "only" and "skip" directives
* @param configFileOpts - Config file options containing directive configurations
* @returns An object with validated "onlyDocDirective" and "skipDocDirective" arrays
* @throws Error if the same directive appears in both "only" and "skip" lists
* @example
* ```typescript
* const cliOptions = { only: ["@example"], skip: ["@internal"] };
* const configOptions = { onlyDocDirective: ["@auth"] };
*
* const visibilityDirectives = getVisibilityDirectives(cliOptions, configOptions);
* console.log(visibilityDirectives);
* // {
* // onlyDocDirective: ["example", "auth"],
* // skipDocDirective: ["internal"]
* // }
*
* // Invalid - will throw an error
* getVisibilityDirectives(
* { only: ["@example"], skip: ["@example"] },
* {}
* ); // Error: The same directive cannot be declared in 'onlyDocDirective' and 'skipDocDirective'.
* ```
* @see {@link getOnlyDocDirectives} and {@link getSkipDocDirectives} for directive retrieval
*/
const getVisibilityDirectives = (cliOpts, configFileOpts) => {
const skipDocDirective = (0, exports.getSkipDocDirectives)(cliOpts, configFileOpts);
const onlyDocDirective = (0, exports.getOnlyDocDirectives)(cliOpts, configFileOpts);
if (onlyDocDirective.some((directiveName) => {
return skipDocDirective.includes(directiveName);
})) {
throw new Error("The same directive cannot be declared in 'onlyDocDirective' and 'skipDocDirective'.");
}
return { onlyDocDirective, skipDocDirective };
};
exports.getVisibilityDirectives = getVisibilityDirectives;
/**
* Processes custom directives, filtering out any that should be skipped.
* Validates that each custom directive has the correct format with required functions.
*
* @param customDirectiveOptions - The custom directive configuration object
* @param skipDocDirective - Array of directive names that should be skipped
* @returns The filtered custom directives object, or undefined if empty/invalid
* @throws Error if a custom directive has an invalid format
* @example
* ```typescript
* // Valid custom directive with tag function
* const customDirectives = {
* example: {
* tag: (value) => `Example: ${value}`
* },
* todo: {
* descriptor: () => "TODO items"
* }
* };
*
* // Filter out the "example" directive
* const filteredDirectives = getCustomDirectives(customDirectives, ["example"]);
* console.log(filteredDirectives); // { todo: { descriptor: [Function] } }
*
* // Invalid format - will throw an error
* getCustomDirectives({ example: { invalid: true } }, []);
* // Error: Wrong format for plugin custom directive "example"...
* ```
* @see {@link DOCS_URL}/advanced/custom-directive for custom directive format documentation
*/
const getCustomDirectives = (customDirectiveOptions, skipDocDirective) => {
if (!customDirectiveOptions ||
Object.keys(customDirectiveOptions).length === 0) {
return undefined;
}
for (const [name, option] of Object.entries(customDirectiveOptions)) {
if (Array.isArray(skipDocDirective) &&
skipDocDirective.includes(name)) {
delete customDirectiveOptions[name];
}
else if (("descriptor" in option && typeof option.descriptor !== "function") ||
("tag" in option && typeof option.tag !== "function") ||
!("tag" in option || "descriptor" in option)) {
throw new Error(`Wrong format for plugin custom directive "${name}".\nPlease refer to ${exports.DOCS_URL}/advanced/custom-directive`);
}
}
return Object.keys(customDirectiveOptions).length === 0
? undefined
: customDirectiveOptions;
};
exports.getCustomDirectives = getCustomDirectives;
/**
* Determines the diff method to use based on the configuration and force flag.
* If force is true, always returns FORCE regardless of the configured diff method.
*
* @param diff - The configured diff method
* @param force - Whether to force regeneration (overrides diff setting)
* @returns The resolved diff method to use
* @example
* ```typescript
* // Normal usage - respects the configured diff method
* const method1 = getDiffMethod(DiffMethod.NONE, false);
* console.log(method1); // "NONE"
*
* // Force flag overrides the diff method
* const method2 = getDiffMethod(DiffMethod.NONE, true);
* console.log(method2); // "FORCE"
* ```
* @see {@link DiffMethod} for available diff methods
*/
const getDiffMethod = (diff, force = false) => {
return force
? DiffMethod.FORCE
: diff.toLocaleUpperCase();
};
exports.getDiffMethod = getDiffMethod;
/**
* Placeholder function for handling deprecated document options.
* Currently returns an empty object as these options are deprecated.
*
* @param _cliOpts - Deprecated CLI options (unused)
* @param _configOptions - Deprecated config options (unused)
* @returns An empty object
*/
const parseDeprecatedDocOptions = (_cliOpts, _configOptions) => {
return {};
};
exports.parseDeprecatedDocOptions = parseDeprecatedDocOptions;
/**
* Builds the document options by merging CLI options, config file options, and defaults.
* Handles index generation flag and front matter configuration.
*
* @param cliOpts - CLI options for document generation
* @param configOptions - Config file options for document generation
* @returns The resolved document options with all required fields
* @example
* ```typescript
* const cliOptions = { index: true };
* const configOptions = { frontMatter: { sidebar_label: 'API' } };
*
* const docOptions = getDocOptions(cliOptions, configOptions);
* console.log(docOptions);
* // {
* // index: true,
* // frontMatter: { sidebar_label: 'API' }
* // }
* ```
*/
const getDocOptions = (cliOpts, configOptions) => {
const deprecated = (0, exports.parseDeprecatedDocOptions)(cliOpts, configOptions);
const index = typeof cliOpts?.index === "boolean"
? cliOpts.index
: typeof configOptions?.index === "boolean"
? configOptions.index
: exports.DEFAULT_OPTIONS.docOptions.index;
return {
frontMatter: {
...deprecated,
...configOptions?.frontMatter,
},
index,
};
};
exports.getDocOptions = getDocOptions;
/**
* Resolves the type hierarchy configuration by merging CLI and config file options.
* Validates that CLI and config don't specify conflicting hierarchy types.
*
* @param cliOption - The hierarchy option specified via CLI (string value)
* @param configOption - The hierarchy option from the config file (string or object)
* @returns The resolved type hierarchy object
* @throws Error if CLI and config specify conflicting hierarchy types
* @example
* ```typescript
* // Using hierarchy from CLI (string format)
* const hierarchy1 = getTypeHierarchyOption("api", undefined);
* console.log(hierarchy1); // { api: {} }
*
* // Using hierarchy from config (object format)
* const hierarchy2 = getTypeHierarchyOption(undefined, { entity: { User: ["posts"] } });
* console.log(hierarchy2); // { entity: { User: ["posts"] } }
*
* // Error case - conflicting hierarchies
* getTypeHierarchyOption("api", { entity: {} });
* // Error: Hierarchy option mismatch in CLI flag 'api' and config 'entity'
* ```
* @see {@link TypeHierarchy} for available hierarchy types
*/
const getTypeHierarchyOption = (cliOption, configOption) => {
const parseValue = (config) => {
if (typeof config === "string") {
switch (true) {
case new RegExp(`^${TypeHierarchy.ENTITY}$`, "i").test(config):
return { [TypeHierarchy.ENTITY]: {} };
case new RegExp(`^${TypeHierarchy.FLAT}$`, "i").test(config):
return { [TypeHierarchy.FLAT]: {} };
case new RegExp(`^${TypeHierarchy.API}$`, "i").test(config):
return { [TypeHierarchy.API]: {} };
default:
return undefined;
}
}
return config;
};
const toStringHierarchy = (hierarchy) => {
return hierarchy && Object.keys(hierarchy)[0];
};
const cliHierarchy = parseValue(cliOption);
const configHierarchy = parseValue(configOption);
if (cliHierarchy && configHierarchy) {
const strCliHierarchy = toStringHierarchy(cliHierarchy);
const strConfigHierarchy = toStringHierarchy(configHierarchy);
if (strCliHierarchy !== strConfigHierarchy) {
throw new Error(`Hierarchy option mismatch in CLI flag '${strCliHierarchy}' and config '${strConfigHierarchy}'`);
}
}
return cliHierarchy ?? configHierarchy ?? exports.DEFAULT_HIERARCHY;
};
exports.getTypeHierarchyOption = getTypeHierarchyOption;
/**
* Placeholder function for handling deprecated print type options.
* Currently returns an empty object as these options are deprecated.
*
* @param _cliOpts - Deprecated CLI options (unused)
* @param _configOptions - Deprecated config options (unused)
* @returns An empty object
*/
const parseDeprecatedPrintTypeOptions = (_cliOpts, _configOptions) => {
return {};
};
exports.parseDeprecatedPrintTypeOptions = parseDeprecatedPrintTypeOptions;
/**
* Builds the print type options by merging CLI options, config file options, and defaults.
* Handles various formatting options for type documentation.
*
* @param cliOpts - CLI options for print type configuration
* @param configOptions - Config file options for print type configuration
* @returns The resolved print type options with all required fields
* @example
* ```typescript
* const cliOptions = { noCode: true, deprecated: "group" };
* const configOptions = {
* exampleSection: true,
* hierarchy: "entity"
* };
*
* const printOptions = getPrintTypeOptions(cliOptions, configOptions);
* console.log(printOptions);
* // {
* // codeSection: false, // Disabled via noCode CLI flag
* // deprecated: "group", // From CLI
* // exampleSection: true, // From config
* // parentTypePrefix: true, // Default value
* // relatedTypeSection: true, // Default value
* // typeBadges: true, // Default value
* // hierarchy: { entity: {} } // Parsed from config
* // }
* ```
* @see {@link DeprecatedOption} for deprecated handling options
* @see {@link getTypeHierarchyOption} for hierarchy resolution
*/
const getPrintTypeOptions = (cliOpts, configOptions) => {
const deprecated = (0, exports.parseDeprecatedPrintTypeOptions)(cliOpts, configOptions);
return {
...deprecated,
codeSection: (!cliOpts?.noCode && configOptions?.codeSection) ??
exports.DEFAULT_OPTIONS.printTypeOptions.codeSection,
deprecated: (cliOpts?.deprecated ??
configOptions?.deprecated ??
exports.DEFAULT_OPTIONS.printTypeOptions.deprecated).toLocaleLowerCase(),
exampleSection: (!cliOpts?.noExample && configOptions?.exampleSection) ??
exports.DEFAULT_OPTIONS.printTypeOptions.exampleSection,
parentTypePrefix: (!cliOpts?.noParentType && configOptions?.parentTypePrefix) ??
exports.DEFAULT_OPTIONS.printTypeOptions.parentTypePrefix,
relatedTypeSection: (!cliOpts?.noRelatedType && configOptions?.relatedTypeSection) ??
exports.DEFAULT_OPTIONS.printTypeOptions.relatedTypeSection,
typeBadges: (!cliOpts?.noTypeBadges && configOptions?.typeBadges) ??
exports.DEFAULT_OPTIONS.printTypeOptions.typeBadges,
hierarchy: (0, exports.getTypeHierarchyOption)(cliOpts?.hierarchy, configOptions?.hierarchy),
};
};
exports.getPrintTypeOptions = getPrintTypeOptions;
/**
* Parses and validates the groupByDirective option string format.
* The format should be \@directive(field|=fallback) where:
* - directive: Name of the directive to group by
* - field: Name of the field in the directive to use for grouping
* - fallback: (Optional) Fallback group name for items without the directive
*
* @param groupOptions - The group directive option as a string
* @returns A parsed GroupByDirectiveOptions object or undefined if invalid
* @throws Error if the groupByDirective format is invalid
* @example
* ```typescript
* // Basic usage with directive and field
* const groupBy1 = parseGroupByOption("@tag(name)");
* console.log(groupBy1);
* // { directive: "tag", field: "name", fallback: "Miscellaneous" }
*
* // With custom fallback group
* const groupBy2 = parseGroupByOption("@category(name|=Other)");
* console.log(groupBy2);
* // { directive: "category", field: "name", fallback: "Other" }
*
* // Invalid format - will throw an error
* parseGroupByOption("invalid-format");
* // Error: Invalid "invalid-format"
* ```
*/
const parseGroupByOption = (groupOptions) => {
const DEFAULT_GROUP = "Miscellaneous";
const OPTION_REGEX = /^@(?<directive>\w+)\((?<field>\w+)(?:\|=(?<fallback>\w+))?\)/;
if (typeof groupOptions !== "string") {
return undefined;
}
const parsedOptions = OPTION_REGEX.exec(groupOptions);
if (typeof parsedOptions === "undefined" || parsedOptions === null) {
throw new Error(`Invalid "${groupOptions}"`);
}
if (!("groups" in parsedOptions)) {
return undefined;
}
const { directive, field, fallback = DEFAULT_GROUP, } = parsedOptions.groups;
return { directive, field, fallback };
};
exports.parseGroupByOption = parseGroupByOption;
const parseHomepageOption = (cliHomepage, configHomepage) => {
if (typeof cliHomepage === "string") {
return cliHomepage;
}
if (configHomepage === false) {
return undefined;
}
if (typeof configHomepage === "string") {
return configHomepage;
}
return exports.DEFAULT_OPTIONS.homepage;
};
exports.parseHomepageOption = parseHomepageOption;
/**
* Builds the complete configuration object by merging options from multiple sources
* in order of precedence:
* 1. CLI options (highest priority)
* 2. Configuration file options
* 3. GraphQL Config options
* 4. Default options (lowest priority)
*
* @param configFileOpts - Options from the configuration file
* @param cliOpts - Options from the command line interface
* @param id - The configuration ID used when referencing multiple schemas
* @returns A promise resolving to the final merged configuration object
* @example
* ```typescript
* // Basic usage with minimal options
* const config = await buildConfig(
* { baseURL: "api" }, // Config file options
* { pretty: true } // CLI options
* );
*
* // With specific config ID
* const config = await buildConfig(
* { schema: "./schemas/users.graphql" },
* { force: true },
* "users"
* );
*
* // The resulting config will contain all required options
* // with values from CLI taking precedence over config file,
* // and defaults filling in any missing values
* ```
* @see {@link Options} for the complete configuration interface
* @see {@link DEFAULT_OPTIONS} for default values
*/
const buildConfig = async (configFileOpts, cliOpts, id = "default") => {
cliOpts ??= {};
const graphqlConfig = await (0, graphql_config_1.loadConfiguration)(id);
const config = (0, deepmerge_1.deepmerge)()({ ...exports.DEFAULT_OPTIONS, ...graphqlConfig }, configFileOpts ?? {});
const baseURL = cliOpts.base ?? config.baseURL;
const { onlyDocDirective, skipDocDirective } = (0, exports.getVisibilityDirectives)(cliOpts, config);
const force = cliOpts.force ?? config.force ?? exports.DEFAULT_OPTIONS.force;
return {
baseURL,
customDirective: (0, exports.getCustomDirectives)(config.customDirective, skipDocDirective),
diffMethod: (0, exports.getDiffMethod)(cliOpts.diff ?? config.diffMethod, force),
docOptions: (0, exports.getDocOptions)(cliOpts, config.docOptions),
force,
groupByDirective: (0, exports.parseGroupByOption)(cliOpts.groupByDirective) ?? config.groupByDirective,
homepageLocation: (0, exports.parseHomepageOption)(cliOpts.homepage, config.homepage),
id: id ?? exports.DEFAULT_OPTIONS.id,
linkRoot: cliOpts.link ?? config.linkRoot ?? exports.DEFAULT_OPTIONS.linkRoot,
loaders: config.loaders,
mdxParser: cliOpts.mdxParser ?? config.mdxParser,
metatags: config.metatags ?? exports.DEFAULT_OPTIONS.metatags,
onlyDocDirective,
outputDir: (0, node_path_1.join)(cliOpts.root ?? config.rootPath, baseURL),
prettify: cliOpts.pretty ?? config.pretty ?? exports.DEFAULT_OPTIONS.pretty,
printer: (config.printer ?? exports.DEFAULT_OPTIONS.printer),
printTypeOptions: (0, exports.getPrintTypeOptions)(cliOpts, config.printTypeOptions),
schemaLocation: cliOpts.schema ?? config.schema ?? exports.DEFAULT_OPTIONS.schema,
skipDocDirective,
tmpDir: cliOpts.tmp ?? config.tmpDir ?? exports.DEFAULT_OPTIONS.tmpDir,
};
};
exports.buildConfig = buildConfig;
;