@mintlify/prebuild
Version:
Helpful functions for Mintlify's prebuild step
301 lines (300 loc) • 14 kB
JavaScript
import { getOpenApiDocumentFromUrl, optionallyAddLeadingSlash, isAllowedLocalSchemaUrl, potentiallyParseOpenApiString, } from '@mintlify/common';
import { generateOpenApiPagesForDocsConfig } from '@mintlify/scraping';
import { findNavGroup } from '@mintlify/scraping/bin/apiPages/common.js';
import { processOpenApiPath, processOpenApiWebhook, } from '@mintlify/scraping/bin/openapi/common.js';
import { divisions, } from '@mintlify/validation';
import * as path from 'path';
const DEFAULT_OUTPUT_DIR = 'api-reference';
export const generateOpenApiFromDocsConfig = async (navigation, openApiFiles, pagesAcc, opts) => {
const { overwrite, writeFiles, targetDir, localSchema } = opts;
const newOpenApiFiles = [];
const openApiFilePromises = new Map();
async function processOpenApiInNav(nav) {
let outputDir = DEFAULT_OUTPUT_DIR;
let openapi;
if ('openapi' in nav) {
if (typeof nav.openapi === 'string') {
openapi = nav.openapi;
}
else if (Array.isArray(nav.openapi) && nav.openapi.length > 0) {
// TODO: handle multiple openapi files
openapi = nav.openapi[0];
}
else if (typeof nav.openapi === 'object' && 'source' in nav.openapi) {
openapi = nav.openapi.source;
outputDir = nav.openapi.directory ?? DEFAULT_OUTPUT_DIR;
}
}
if (openapi) {
let openApiFile = undefined;
if (isAllowedLocalSchemaUrl(openapi, localSchema)) {
if (!openApiFilePromises.has(openapi)) {
const promise = createOpenApiFile(openapi);
openApiFilePromises.set(openapi, promise);
openApiFile = await promise;
newOpenApiFiles.push(openApiFile);
}
else {
openApiFile = await openApiFilePromises.get(openapi);
}
}
else {
openApiFile = openApiFiles.find((file) => file.originalFileLocation != undefined &&
file.originalFileLocation === optionallyAddLeadingSlash(openapi));
}
if (!openApiFile) {
throw new Error(`Openapi file ${openapi} defined in ${getDivisionNav(nav)
?.division} in your docs.json does not exist`);
}
const { pagesAcc: pagesAccFromGeneratedOpenApiPages, nav: navFromGeneratedOpenApiPages } = await generateOpenApiPagesForDocsConfig(openApiFile.spec, {
openApiFilePath: openApiFile.originalFileLocation,
writeFiles,
outDir: outputDir,
outDirBasePath: path.join(targetDir ?? '', 'src', '_props'),
overwrite,
localSchema,
});
Object.entries(pagesAccFromGeneratedOpenApiPages).forEach(([key, value]) => {
pagesAcc[key] = value;
});
const divisionNav = getDivisionNav(nav);
if (divisionNav?.division) {
return {
[divisionNav.division]: divisionNav.name,
...divisionNav.nav,
...(divisionNav.division === 'group'
? {
pages: 'pages' in nav
? [...nav.pages, ...navFromGeneratedOpenApiPages]
: navFromGeneratedOpenApiPages,
}
: {
groups: 'groups' in nav
? [...nav.groups, ...navFromGeneratedOpenApiPages]
: navFromGeneratedOpenApiPages,
}),
};
}
}
return null;
}
function extractOpenApiFromNav(nav) {
if ('openapi' in nav && nav.openapi !== null) {
const openapiProp = nav.openapi;
if (typeof openapiProp === 'string') {
return { source: openapiProp, directory: undefined };
}
else if (Array.isArray(openapiProp) && openapiProp.length > 0) {
return { source: openapiProp[0], directory: undefined };
}
else if (typeof openapiProp === 'object' &&
'source' in openapiProp &&
typeof openapiProp.source === 'string') {
const directory = 'directory' in openapiProp && typeof openapiProp.directory === 'string'
? openapiProp.directory
: undefined;
return {
source: openapiProp.source,
directory,
};
}
}
return { source: undefined, directory: undefined };
}
const skipBulkForNode = new Set();
const skipBulkForNodeId = new Set();
let numNodes = 0;
async function processNav(nav, inheritedOpenApi, inheritedDirectory, openApiOwner) {
const nodeId = numNodes++;
const extracted = extractOpenApiFromNav(nav);
const currentOpenApi = extracted.source ?? inheritedOpenApi;
const currentDirectory = extracted.source
? extracted.directory
: extracted.directory ?? inheritedDirectory;
const currentOpenApiOwner = extracted.source ? nodeId : openApiOwner;
let newNav = nav;
if ('pages' in newNav) {
newNav.pages = await Promise.all(newNav.pages.map(async (page) => {
if (typeof page === 'object' && page !== null && 'group' in page) {
return processNav(page, currentOpenApi, currentDirectory, currentOpenApiOwner);
}
if (typeof page !== 'string') {
return page;
}
const parsed = potentiallyParseOpenApiString(page);
if (parsed) {
const { filename: explicitOpenapiPath, method, endpoint } = parsed;
const openapiPath = explicitOpenapiPath ?? currentOpenApi;
if (!openapiPath) {
}
else {
let openApiFile;
if (openapiPath) {
if (isAllowedLocalSchemaUrl(openapiPath, localSchema)) {
if (!openApiFilePromises.has(openapiPath)) {
const promise = createOpenApiFile(openapiPath);
openApiFilePromises.set(openapiPath, promise);
openApiFile = await promise;
newOpenApiFiles.push(openApiFile);
}
else {
openApiFile = await openApiFilePromises.get(openapiPath);
}
}
else {
openApiFile = openApiFiles.find((file) => file.originalFileLocation != undefined &&
file.originalFileLocation === optionallyAddLeadingSlash(openapiPath));
}
}
if (!openApiFile) {
throw new Error(`Openapi file ${openapiPath} referenced in docs.json does not exist`);
}
const schema = openApiFile.spec;
const isWebhook = method.toLowerCase() === 'webhook';
const tempNav = [];
const tempDecoratedNav = [];
const writePromises = [];
if (isWebhook) {
const webhookObject = schema.webhooks?.[endpoint];
if (!webhookObject || typeof webhookObject !== 'object') {
throw new Error(`Webhook ${endpoint} not found in ${openapiPath}`);
}
processOpenApiWebhook(endpoint, webhookObject, schema, tempNav, tempDecoratedNav, writePromises, pagesAcc, {
openApiFilePath: openApiFile.originalFileLocation,
writeFiles,
outDir: currentDirectory ?? DEFAULT_OUTPUT_DIR,
outDirBasePath: path.join(targetDir ?? '', 'src', '_props'),
overwrite,
localSchema,
}, findNavGroup);
}
else {
const pathItemObject = (schema.paths ?? {})[endpoint];
if (!pathItemObject || typeof pathItemObject !== 'object') {
throw new Error(`Endpoint ${endpoint} not found in ${openapiPath}`);
}
const opObject = pathItemObject[method.toLowerCase()];
if (!opObject) {
throw new Error(`Method ${method.toUpperCase()} for endpoint ${endpoint} not found in ${openapiPath}`);
}
processOpenApiPath(endpoint, { [method.toLowerCase()]: opObject }, schema, tempNav, tempDecoratedNav, writePromises, pagesAcc, {
openApiFilePath: openApiFile.originalFileLocation,
writeFiles,
outDir: currentDirectory ?? DEFAULT_OUTPUT_DIR,
outDirBasePath: path.join(targetDir ?? '', 'src', '_props'),
overwrite,
localSchema,
}, findNavGroup);
}
await Promise.all(writePromises);
let slug;
const firstEntry = tempNav[0];
if (firstEntry &&
typeof firstEntry === 'object' &&
'pages' in firstEntry &&
Array.isArray(firstEntry.pages) &&
firstEntry.pages.length > 0) {
slug = firstEntry.pages[0];
}
else if (typeof firstEntry === 'string') {
slug = firstEntry;
}
else {
slug = Object.keys(pagesAcc).pop();
}
if (!slug) {
throw new Error('Failed to generate OpenAPI endpoint page');
}
if (currentOpenApiOwner && !explicitOpenapiPath) {
skipBulkForNodeId.add(currentOpenApiOwner);
}
return slug;
}
}
return page;
}));
}
for (const division of ['groups', ...divisions]) {
if (division in newNav) {
const items = newNav[division];
newNav = {
...newNav,
[division]: await Promise.all(items.map((item) => processNav(item, currentOpenApi, currentDirectory, currentOpenApiOwner))),
};
}
}
if (skipBulkForNodeId.has(nodeId)) {
skipBulkForNode.add(newNav);
}
return newNav;
}
const navAfterExplicit = await processNav(navigation, undefined, undefined, undefined);
async function generateBulkNav(node) {
let updated = node;
if (extractOpenApiFromNav(updated).source && !skipBulkForNode.has(updated)) {
const processed = await processOpenApiInNav(updated);
if (processed)
updated = processed;
}
if ('pages' in updated) {
updated.pages = await Promise.all(updated.pages.map(async (p) => {
if (typeof p === 'object' && p !== null && 'group' in p) {
return generateBulkNav(p);
}
return p;
}));
}
for (const division of ['groups', ...divisions]) {
if (division in updated) {
const items = updated[division];
updated = {
...updated,
[division]: await Promise.all(items.map((child) => generateBulkNav(child))),
};
}
}
return updated;
}
const processedNavigation = await generateBulkNav(navAfterExplicit);
navigation = processedNavigation;
return {
newNav: processedNavigation,
newOpenApiFiles,
};
};
function getDivisionNav(nav) {
if ('openapi' in nav) {
const { openapi: _, ...updatedNav } = nav;
const divisionMap = {
group: 'group',
anchor: 'anchor',
tab: 'tab',
version: 'version',
language: 'language',
dropdown: 'dropdown',
product: 'product',
item: 'item',
};
const divisionType = Object.keys(divisionMap).find((key) => key in updatedNav);
return {
division: divisionMap[divisionType],
name: updatedNav[divisionType],
nav: updatedNav,
};
}
return undefined;
}
async function createOpenApiFile(openApiUrl) {
try {
const document = await getOpenApiDocumentFromUrl(openApiUrl);
return {
filename: openApiUrl,
spec: document,
originalFileLocation: openApiUrl,
};
}
catch (err) {
console.error(err);
throw err;
}
}