UNPKG

@redocly/openapi-core

Version:

See https://github.com/Redocly/redocly-cli

323 lines 15.7 kB
import * as path from 'node:path'; import * as url from 'node:url'; import * as fs from 'node:fs'; import module from 'node:module'; import { isAbsoluteUrl } from '../ref-utils.js'; import { isNotString } from '../utils/is-not-string.js'; import { isString } from '../utils/is-string.js'; import { isPlainObject } from '../utils/is-plain-object.js'; import { isDefined } from '../utils/is-defined.js'; import { resolveDocument, BaseResolver, Source } from '../resolve.js'; import { defaultPlugin } from './builtIn.js'; import { deepCloneMapWithJSON, isCommonJsPlugin, isDeprecatedPluginFormat, mergeExtends, parsePresetName, prefixRules, } from './utils.js'; import { getResolveConfig } from './get-resolve-config.js'; import { isBrowser } from '../env.js'; import { colorize, logger } from '../logger.js'; import { NormalizedConfigTypes } from '../types/redocly-yaml.js'; import { bundleConfig, collectConfigPlugins } from '../bundle/bundle.js'; import { CONFIG_FILE_NAME, DEFAULT_CONFIG, DEFAULT_PROJECT_PLUGIN_PATHS } from './constants.js'; // Cache instantiated plugins during a single execution const pluginsCache = new Map(); export async function resolveConfig({ rawConfigDocument, configPath, externalRefResolver, customExtends, }) { const config = rawConfigDocument === undefined ? DEFAULT_CONFIG : rawConfigDocument.parsed; if (customExtends !== undefined && isPlainObject(config)) { config.extends = customExtends; } if (isPlainObject(config) && config?.extends?.some(isNotString)) { throw new Error(`Configuration format not detected in extends: values must be strings.`); } const rootDocument = rawConfigDocument ?? { source: new Source(configPath ?? '', JSON.stringify(config)), parsed: config, }; const resolvedRefMap = await resolveDocument({ rootDocument, rootType: NormalizedConfigTypes.ConfigRoot, externalRefResolver: externalRefResolver ?? new BaseResolver(getResolveConfig(config?.resolve)), }); let pluginsOrPaths = []; let resolvedPlugins; let rootConfigDir = ''; if (isBrowser) { // In browser, we don't support plugins from config file yet const instantiatedPlugins = (config?.plugins || []).filter((p) => !isString(p)); resolvedPlugins = [...instantiatedPlugins, defaultPlugin]; } else { rootConfigDir = path.dirname(configPath ?? ''); pluginsOrPaths = collectConfigPlugins(rootDocument, resolvedRefMap, rootConfigDir); const plugins = await resolvePlugins(pluginsOrPaths.map((p) => (isPluginResolveInfo(p) ? p.absolutePath : p)), rootConfigDir); resolvedPlugins = [...plugins, defaultPlugin]; } const bundledConfig = bundleConfig(rootDocument, deepCloneMapWithJSON(resolvedRefMap), resolvedPlugins); if (bundledConfig.apis) { bundledConfig.apis = Object.fromEntries(Object.entries(bundledConfig.apis).map(([key, apiConfig]) => { const mergedConfig = mergeExtends([bundledConfig, apiConfig]); return [key, { ...apiConfig, ...mergedConfig }]; })); } const pluginPaths = pluginsOrPaths.length ? pluginsOrPaths .map((p) => isPluginResolveInfo(p) && p.isModule ? p.rawPath : p.absolutePath && path.relative(rootConfigDir, p.absolutePath)) .filter(isDefined) : undefined; return { resolvedConfig: { ...bundledConfig, plugins: pluginPaths, }, resolvedRefMap, plugins: resolvedPlugins, }; } function getDefaultPluginPath(configDir) { for (const pluginPath of DEFAULT_PROJECT_PLUGIN_PATHS) { const absolutePluginPath = path.resolve(configDir, pluginPath); if (fs.existsSync(absolutePluginPath)) { return pluginPath; } } return; } function isPluginResolveInfo(plugin) { return 'isModule' in plugin; } export const preResolvePluginPath = (plugin, base, rootConfigDir) => { if (!isString(plugin)) { return plugin; } const maybeAbsolutePluginPath = path.resolve(path.dirname(base), plugin); return fs.existsSync(maybeAbsolutePluginPath) ? { absolutePath: maybeAbsolutePluginPath, rawPath: plugin, isModule: false } : { absolutePath: module.createRequire(import.meta.url ?? __dirname).resolve(plugin, { paths: [ // Plugins imported from the node_modules in the project directory rootConfigDir, // Plugins imported from the node_modules in the package install directory (for example, npx cache directory) import.meta.url ? path.dirname(url.fileURLToPath(import.meta.url)) : __dirname, ], }), isModule: true, rawPath: plugin, }; }; export async function resolvePlugins(plugins, configDir) { if (!plugins) return []; // TODO: implement or reuse Resolver approach so it will work in node and browser envs const requireFunc = async (plugin) => { if (!isString(plugin)) { return plugin; } try { const absolutePluginPath = path.isAbsolute(plugin) ? plugin : preResolvePluginPath(plugin, path.join(configDir, CONFIG_FILE_NAME), configDir).absolutePath; if (!pluginsCache.has(absolutePluginPath)) { let requiredPlugin; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore FIXME: investigate if we still need this (2.0) if (typeof __webpack_require__ === 'function') { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore FIXME: investigate if we still need this (2.0) requiredPlugin = __non_webpack_require__(absolutePluginPath); } else { const mod = await import(url.pathToFileURL(absolutePluginPath).pathname); requiredPlugin = mod.default || mod; } const pluginCreatorOptions = { contentDir: configDir }; const requiredPluginInstances = Array.isArray(requiredPlugin) ? requiredPlugin : [requiredPlugin]; for (const requiredPluginInstance of requiredPluginInstances) { if (requiredPluginInstance?.id && isDeprecatedPluginFormat(requiredPluginInstance)) { logger.info(`Deprecated plugin format detected: ${requiredPluginInstance.id}\n`); } } const pluginModule = isDeprecatedPluginFormat(requiredPlugin) ? requiredPlugin : isCommonJsPlugin(requiredPlugin) ? await requiredPlugin(pluginCreatorOptions) : await requiredPlugin?.default?.(pluginCreatorOptions); const pluginInstances = Array.isArray(pluginModule) ? pluginModule : [pluginModule]; if (pluginModule) { pluginsCache.set(absolutePluginPath, pluginInstances.map((p) => ({ ...p, path: plugin, absolutePath: absolutePluginPath, }))); } } return pluginsCache.get(absolutePluginPath); } catch (e) { throw new Error(`Failed to load plugin "${plugin}": ${e.message}\n\n${e.stack}`); } }; const seenPluginIds = new Map(); /** * Include the default plugin automatically if it's not in configuration */ const defaultPluginPath = getDefaultPluginPath(configDir); if (defaultPluginPath) { plugins.push(defaultPluginPath); } const resolvedPlugins = new Set(); const instances = await Promise.all(plugins.map(async (p) => { if (isString(p)) { if (isAbsoluteUrl(p)) { throw new Error(colorize.red(`We don't support remote plugins yet.`)); } if (resolvedPlugins.has(p)) { return; } resolvedPlugins.add(p); } const pluginInstanceOrInstances = await requireFunc(p); if (!pluginInstanceOrInstances) { return; } const pluginInstances = Array.isArray(pluginInstanceOrInstances) ? pluginInstanceOrInstances : [pluginInstanceOrInstances]; return (await Promise.all(pluginInstances.map(async (pluginInstance) => { if (!pluginInstance) return; const id = pluginInstance.id; if (typeof id !== 'string') { throw new Error(colorize.red(`Plugin must define \`id\` property in ${colorize.blue(p.toString())}.`)); } const pluginPath = pluginInstance.absolutePath ?? p.toString(); const existingPluginPath = seenPluginIds.get(id); if (existingPluginPath) { if (pluginPath !== existingPluginPath) { throw new Error(colorize.red(`Plugin "id" must be unique. Plugin ${colorize.blue(pluginPath)} uses id "${colorize.blue(id)}" already seen in ${colorize.blue(pluginPath)}`)); } return undefined; } seenPluginIds.set(id, pluginPath); const plugin = { id, ...(pluginInstance.configs ? { configs: pluginInstance.configs } : {}), ...(pluginInstance.typeExtension ? { typeExtension: pluginInstance.typeExtension } : {}), }; if (pluginInstance.rules) { if (!pluginInstance.rules.oas3 && !pluginInstance.rules.oas2 && !pluginInstance.rules.async2 && !pluginInstance.rules.async3 && !pluginInstance.rules.arazzo1 && !pluginInstance.rules.overlay1) { throw new Error(`Plugin rules must have \`oas3\`, \`oas2\`, \`async2\`, \`async3\`, \`arazzo\`, or \`overlay1\` rules "${p}.`); } plugin.rules = {}; if (pluginInstance.rules.oas3) { plugin.rules.oas3 = prefixRules(pluginInstance.rules.oas3, id); } if (pluginInstance.rules.oas2) { plugin.rules.oas2 = prefixRules(pluginInstance.rules.oas2, id); } if (pluginInstance.rules.async2) { plugin.rules.async2 = prefixRules(pluginInstance.rules.async2, id); } if (pluginInstance.rules.async3) { plugin.rules.async3 = prefixRules(pluginInstance.rules.async3, id); } if (pluginInstance.rules.arazzo1) { plugin.rules.arazzo1 = prefixRules(pluginInstance.rules.arazzo1, id); } if (pluginInstance.rules.overlay1) { plugin.rules.overlay1 = prefixRules(pluginInstance.rules.overlay1, id); } } if (pluginInstance.preprocessors) { if (!pluginInstance.preprocessors.oas3 && !pluginInstance.preprocessors.oas2 && !pluginInstance.preprocessors.async2 && !pluginInstance.preprocessors.async3 && !pluginInstance.preprocessors.arazzo1 && !pluginInstance.preprocessors.overlay1) { throw new Error(`Plugin \`preprocessors\` must have \`oas3\`, \`oas2\`, \`async2\`, \`async3\`, \`arazzo1\`, or \`overlay1\` preprocessors "${p}.`); } plugin.preprocessors = {}; if (pluginInstance.preprocessors.oas3) { plugin.preprocessors.oas3 = prefixRules(pluginInstance.preprocessors.oas3, id); } if (pluginInstance.preprocessors.oas2) { plugin.preprocessors.oas2 = prefixRules(pluginInstance.preprocessors.oas2, id); } if (pluginInstance.preprocessors.async2) { plugin.preprocessors.async2 = prefixRules(pluginInstance.preprocessors.async2, id); } if (pluginInstance.preprocessors.async3) { plugin.preprocessors.async3 = prefixRules(pluginInstance.preprocessors.async3, id); } if (pluginInstance.preprocessors.arazzo1) { plugin.preprocessors.arazzo1 = prefixRules(pluginInstance.preprocessors.arazzo1, id); } if (pluginInstance.preprocessors.overlay1) { plugin.preprocessors.overlay1 = prefixRules(pluginInstance.preprocessors.overlay1, id); } } if (pluginInstance.decorators) { if (!pluginInstance.decorators.oas3 && !pluginInstance.decorators.oas2 && !pluginInstance.decorators.async2 && !pluginInstance.decorators.async3 && !pluginInstance.decorators.arazzo1 && !pluginInstance.decorators.overlay1) { throw new Error(`Plugin \`decorators\` must have \`oas3\`, \`oas2\`, \`async2\`, \`async3\`, \`arazzo1\`, or \`overlay1\` decorators "${p}.`); } plugin.decorators = {}; if (pluginInstance.decorators.oas3) { plugin.decorators.oas3 = prefixRules(pluginInstance.decorators.oas3, id); } if (pluginInstance.decorators.oas2) { plugin.decorators.oas2 = prefixRules(pluginInstance.decorators.oas2, id); } if (pluginInstance.decorators.async2) { plugin.decorators.async2 = prefixRules(pluginInstance.decorators.async2, id); } if (pluginInstance.decorators.async3) { plugin.decorators.async3 = prefixRules(pluginInstance.decorators.async3, id); } if (pluginInstance.decorators.arazzo1) { plugin.decorators.arazzo1 = prefixRules(pluginInstance.decorators.arazzo1, id); } if (pluginInstance.decorators.overlay1) { plugin.decorators.overlay1 = prefixRules(pluginInstance.decorators.overlay1, id); } } if (pluginInstance.assertions) { plugin.assertions = pluginInstance.assertions; } return { ...pluginInstance, ...plugin, }; }))).filter(isDefined); })); return instances.filter(isDefined).flat(); } export function resolvePreset(presetName, plugins) { const { pluginId, configName } = parsePresetName(presetName); const plugin = plugins.find((p) => p.id === pluginId); if (!plugin) { throw new Error(`Invalid config ${colorize.red(presetName)}: plugin ${pluginId} is not included.`); } const preset = plugin.configs?.[configName]; if (!preset) { throw new Error(pluginId ? `Invalid config ${colorize.red(presetName)}: plugin ${pluginId} doesn't export config with name ${configName}.` : `Invalid config ${colorize.red(presetName)}: there is no such built-in config.`); } return preset; } //# sourceMappingURL=config-resolvers.js.map