UNPKG

@graphql-markdown/core

Version:

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

605 lines (604 loc) 25.3 kB
"use strict"; /** * 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.parseDeprecatedPrintTypeOptions = exports.parseDeprecatedFormatterOption = exports.getTypeHierarchyOption = exports.getDocOptions = exports.parseDeprecatedDocOptions = exports.getDiffMethod = exports.getForcedDiffMethod = 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 logger_1 = require("@graphql-markdown/logger"); const graphql_config_1 = require("./graphql-config"); const patterns_1 = require("./const/patterns"); const validation_1 = require("./directives/validation"); const builder_1 = require("./options/builder"); /** * 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: { categorySort: undefined, frontMatter: {}, index: false, sectionHeaderId: true, }, force: false, groupByDirective: undefined, homepage: exports.ASSET_HOMEPAGE_LOCATION, linkRoot: "/", loaders: undefined, metatags: [], pretty: false, printTypeOptions: { deprecated: DeprecatedOption.DEFAULT, exampleSection: undefined, parentTypePrefix: 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) => { if (typeof name !== "string" || !patterns_1.PATTERNS.DIRECTIVE_NAME.test(name)) { throw new Error(`Invalid "${name}"`); } const { groups: { directive }, } = patterns_1.PATTERNS.DIRECTIVE_NAME.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?.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 descriptor function * const customDirectives = { * example: { * tag: (value) => `Example: ${value}` * }, * note: { * descriptor: () => "Note items" * } * }; * * // Filter out the "example" directive, keeping "note" * const filteredDirectives = getCustomDirectives(customDirectives, ["example"]); * console.log(filteredDirectives); // { note: { 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 ((0, validation_1.isInvalidFunctionProperty)(option, "descriptor") || (0, validation_1.isInvalidFunctionProperty)(option, "tag") || !("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 ? customDirectiveOptions : undefined; }; exports.getCustomDirectives = getCustomDirectives; /** * Returns FORCE as the diff method. * This function is used when documentation should be forcefully regenerated. * * @returns The FORCE diff method * @example * ```typescript * const method = getForcedDiffMethod(); * console.log(method); // "FORCE" * ``` * @see {@link DiffMethod} for available diff methods */ const getForcedDiffMethod = () => { return DiffMethod.FORCE; }; exports.getForcedDiffMethod = getForcedDiffMethod; const getNormalizedDiffMethod = (diff) => { return diff.toLocaleUpperCase(); }; const getDiffMethod = (diff) => { return getNormalizedDiffMethod(diff); }; exports.getDiffMethod = getDiffMethod; 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 cliIndex = typeof cliOpts?.index === "boolean" ? cliOpts.index : undefined; const configIndex = typeof configOptions?.index === "boolean" ? configOptions.index : undefined; const index = cliIndex ?? configIndex ?? exports.DEFAULT_OPTIONS.docOptions.index; const configSectionHeaderId = typeof configOptions?.sectionHeaderId === "boolean" ? configOptions.sectionHeaderId : exports.DEFAULT_OPTIONS.docOptions.sectionHeaderId; const sectionHeaderId = cliOpts?.noSectionId === true ? false : configSectionHeaderId; return { categorySort: configOptions?.categorySort, frontMatter: { ...deprecated, ...configOptions?.frontMatter, }, index, sectionHeaderId, }; }; 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; const parseDeprecatedFormatterOption = (cliOpts, configOptions) => { const legacyCli = cliOpts?.mdxParser ?? null; // NOSONAR typescript:S1874 const legacyConfig = configOptions?.mdxParser; // NOSONAR typescript:S1874 if (legacyCli !== null || (legacyConfig !== undefined && legacyConfig !== null)) { (0, logger_1.log)(`Setting "mdxParser" is deprecated and will be removed in a future version. Use "formatter" instead.`, logger_1.LogLevel.warn); } return (cliOpts?.formatter ?? legacyCli ?? configOptions?.formatter ?? legacyConfig); }; exports.parseDeprecatedFormatterOption = parseDeprecatedFormatterOption; 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 = { deprecated: "group" }; * const configOptions = { * exampleSection: { directive: "example" }, * hierarchy: "entity" * }; * * const printOptions = getPrintTypeOptions(cliOptions, configOptions); * console.log(printOptions); * // { * // deprecated: "group", // From CLI * // exampleSection: { directive: "example" }, // From config * // parentTypePrefix: 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) => { return { deprecated: (cliOpts?.deprecated ?? configOptions?.deprecated ?? exports.DEFAULT_OPTIONS.printTypeOptions.deprecated).toLocaleLowerCase(), exampleSection: configOptions?.exampleSection ?? exports.DEFAULT_OPTIONS.printTypeOptions.exampleSection, parentTypePrefix: (!cliOpts?.noParentType && configOptions?.parentTypePrefix) ?? exports.DEFAULT_OPTIONS.printTypeOptions.parentTypePrefix, typeBadges: (!cliOpts?.noTypeBadges && configOptions?.typeBadges) ?? exports.DEFAULT_OPTIONS.printTypeOptions.typeBadges, hierarchy: (0, exports.getTypeHierarchyOption)(cliOpts?.hierarchy, configOptions?.hierarchy), }; }; /** * 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) => { if (typeof groupOptions !== "string") { return undefined; } const parsedOptions = patterns_1.PATTERNS.GROUP_BY_DIRECTIVE.exec(groupOptions); if (parsedOptions === null) { throw new Error(`Invalid "${groupOptions}"`); } return { directive: parsedOptions[1], field: parsedOptions[2], fallback: parsedOptions[3] || patterns_1.CONFIG_CONSTANTS.DEFAULT_GROUP, }; }; 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; const buildConfig = async (configFileOpts, cliOpts, id = "default") => { cliOpts ??= {}; const graphqlConfig = await (0, graphql_config_1.loadConfiguration)(id); (0, exports.parseDeprecatedPrintTypeOptions)(cliOpts, { ...graphqlConfig?.printTypeOptions, ...configFileOpts?.printTypeOptions, }); const config = (0, deepmerge_1.deepmerge)()({ ...exports.DEFAULT_OPTIONS, ...graphqlConfig }, configFileOpts ?? {}); const { onlyDocDirective, skipDocDirective } = (0, exports.getVisibilityDirectives)(cliOpts, config); // Build options using consistent precedence: CLI > Config > Default // The precedence is enforced by the OptionBuilder's setIfProvided method // which only updates values if the source has higher precedence than the current source const { baseURL, force, linkRoot, prettify, rootPath, schemaLocation, tmpDir, } = new builder_1.OptionBuilder() .addDefault(exports.DEFAULT_OPTIONS.baseURL, "baseURL") .addFromConfig(config.baseURL, "baseURL") .addFromCli(cliOpts.base, "baseURL") .addDefault(exports.DEFAULT_OPTIONS.force, "force") .addFromConfig(config.force, "force") .addFromCli(cliOpts.force, "force") .addDefault(exports.DEFAULT_OPTIONS.linkRoot, "linkRoot") .addFromConfig(config.linkRoot, "linkRoot") .addFromCli(cliOpts.link, "linkRoot") .addDefault(exports.DEFAULT_OPTIONS.pretty, "prettify") .addFromConfig(config.pretty, "prettify") .addFromCli(cliOpts.pretty, "prettify") .addDefault(exports.DEFAULT_OPTIONS.rootPath, "rootPath") .addFromConfig(config.rootPath, "rootPath") .addFromCli(cliOpts.root, "rootPath") .addDefault(exports.DEFAULT_OPTIONS.schema, "schemaLocation") .addFromConfig(config.schema, "schemaLocation") .addFromCli(cliOpts.schema, "schemaLocation") .addDefault(exports.DEFAULT_OPTIONS.tmpDir, "tmpDir") .addFromConfig(config.tmpDir, "tmpDir") .addFromCli(cliOpts.tmp, "tmpDir") .build(); return { baseURL, customDirective: (0, exports.getCustomDirectives)(config.customDirective, skipDocDirective), diffMethod: force ? (0, exports.getForcedDiffMethod)() : (0, exports.getDiffMethod)(cliOpts.diff ?? config.diffMethod), 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, loaders: config.loaders, formatter: (0, exports.parseDeprecatedFormatterOption)(cliOpts, config), metatags: config.metatags ?? exports.DEFAULT_OPTIONS.metatags, onlyDocDirective, outputDir: (0, node_path_1.join)(rootPath, baseURL), prettify, printTypeOptions: getPrintTypeOptions(cliOpts, config.printTypeOptions), schemaLocation, skipDocDirective, tmpDir, }; }; exports.buildConfig = buildConfig;