@redocly/cli
Version:
[@Redocly](https://redocly.com) CLI is your all-in-one API documentation utility. It builds, manages, improves, and quality-checks your API descriptions, all of which comes in handy for various phases of the API Lifecycle. Create your own rulesets to make
141 lines • 6.73 kB
JavaScript
import { BaseResolver, formatProblems, getTotals, detectSpec, bundleDocument, logger, isPlainObject, isEmptyObject, getTypes, } from '@redocly/openapi-core';
import { blue, yellow } from 'colorette';
import * as path from 'node:path';
import { performance } from 'node:perf_hooks';
import { exitWithError } from '../../utils/error.js';
import { getFallbackApisOrExit, printExecutionTime, sortTopLevelKeysForOas, getAndValidateFileExtension, writeToFileByExtension, } from '../../utils/miscellaneous.js';
import { COMPONENTS } from '../split/types.js';
import { replace$Refs, getInfoPrefix, getApiFilename, iteratePotentialConflicts, populateTags, collectExternalDocs, collectPaths, collectComponents, collectWebhooks, addInfoSectionAndSpecVersion, } from './helpers/index.js';
export async function handleJoin({ argv, config, version: packageVersion, collectSpecData, }) {
const startedAt = performance.now();
const { 'prefix-components-with-info-prop': prefixComponentsWithInfoProp, 'prefix-tags-with-filename': prefixTagsWithFilename, 'prefix-tags-with-info-prop': prefixTagsWithInfoProp, 'without-x-tag-groups': withoutXTagGroups, output, } = argv;
const usedTagsOptions = [
prefixTagsWithFilename && 'prefix-tags-with-filename',
prefixTagsWithInfoProp && 'prefix-tags-with-info-prop',
withoutXTagGroups && 'without-x-tag-groups',
].filter(Boolean);
if (usedTagsOptions.length > 1) {
return exitWithError(`You use ${yellow(usedTagsOptions.join(', '))} together.\nPlease choose only one!`);
}
const apis = await getFallbackApisOrExit(argv.apis, config);
if (apis.length < 2) {
return exitWithError(`At least 2 APIs should be provided.`);
}
const fileExtension = getAndValidateFileExtension(output || apis[0].path);
const specFilename = output || `openapi.${fileExtension}`;
const externalRefResolver = new BaseResolver(config.resolve);
const documents = await Promise.all(apis.map(({ path }) => externalRefResolver.resolveDocument(null, path, true)));
const decorators = new Set([
...Object.keys(config.decorators.oas3_0),
...Object.keys(config.decorators.oas3_1),
...Object.keys(config.decorators.oas2),
]);
config.skipDecorators(Array.from(decorators));
const preprocessors = new Set([
...Object.keys(config.preprocessors.oas3_0),
...Object.keys(config.preprocessors.oas3_1),
...Object.keys(config.preprocessors.oas2),
]);
config.skipPreprocessors(Array.from(preprocessors));
const bundleResults = await Promise.all(documents.map((document) => bundleDocument({
document,
config,
externalRefResolver: new BaseResolver(config.resolve),
types: getTypes(detectSpec(document.parsed)),
}).catch((e) => {
exitWithError(`${e.message}: ${blue(document.source.absoluteRef)}`);
})));
for (const { problems, bundle: document } of bundleResults) {
const fileTotals = getTotals(problems);
if (fileTotals.errors) {
formatProblems(problems, {
totals: fileTotals,
version: packageVersion,
command: 'join',
});
exitWithError(`❌ Errors encountered while bundling ${blue(document.source.absoluteRef)}: join will not proceed.`);
}
}
let oasVersion = null;
for (const document of documents) {
try {
const version = detectSpec(document.parsed);
collectSpecData?.(document.parsed);
if (version !== 'oas3_0' && version !== 'oas3_1' && version !== 'oas3_2') {
return exitWithError(`Only OpenAPI 3.0, 3.1, and 3.2 are supported: ${blue(document.source.absoluteRef)}.`);
}
oasVersion = oasVersion ?? version;
if (oasVersion !== version) {
return exitWithError(`All APIs must use the same OpenAPI version: ${blue(document.source.absoluteRef)}.`);
}
}
catch (e) {
return exitWithError(`${e.message}: ${blue(document.source.absoluteRef)}.`);
}
}
const [first, ...others] = (documents ?? []);
const serversAreTheSame = others.every(({ parsed: { paths, servers } }) => {
// include only documents with paths
if (!paths || isEmptyObject(paths || {})) {
return true;
}
return servers?.every((server) => first.parsed.servers?.find(({ url }) => url === server.url));
});
const joinedDef = {};
const potentialConflicts = {
tags: {},
paths: {},
components: {},
webhooks: {},
};
addInfoSectionAndSpecVersion(joinedDef, documents, prefixComponentsWithInfoProp);
if (serversAreTheSame && first.parsed.servers) {
joinedDef.servers = first.parsed.servers;
}
for (const document of documents) {
const openapi = isPlainObject(document.parsed)
? document.parsed
: {};
const { tags, info } = openapi;
const api = path.relative(process.cwd(), document.source.absoluteRef);
const apiFilename = getApiFilename(api);
const tagsPrefix = prefixTagsWithFilename
? apiFilename
: getInfoPrefix(info, prefixTagsWithInfoProp, 'tags');
const componentsPrefix = getInfoPrefix(info, prefixComponentsWithInfoProp, COMPONENTS);
if (openapi.hasOwnProperty('x-tagGroups')) {
logger.warn(`warning: x-tagGroups at ${blue(api)} will be skipped \n`);
}
const context = {
api,
apiFilename,
apiTitle: info?.title,
tags,
potentialConflicts,
tagsPrefix,
componentsPrefix,
oasVersion,
};
if (tags) {
populateTags({ joinedDef, withoutXTagGroups, context });
}
collectExternalDocs({ joinedDef, openapi, context });
collectPaths({ joinedDef, withoutXTagGroups, openapi, context, serversAreTheSame });
collectComponents({ joinedDef, openapi, context });
collectWebhooks({ joinedDef, withoutXTagGroups, openapi, context });
if (componentsPrefix) {
replace$Refs(openapi, componentsPrefix);
}
}
const potentialConflictsTotal = iteratePotentialConflicts({
potentialConflicts,
withoutXTagGroups,
});
const noRefs = true;
if (potentialConflictsTotal) {
return exitWithError(`Please fix conflicts before running ${yellow('join')}.`);
}
writeToFileByExtension(sortTopLevelKeysForOas(joinedDef), specFilename, noRefs);
printExecutionTime('join', startedAt, specFilename);
}
//# sourceMappingURL=index.js.map