UNPKG

@mintlify/prebuild

Version:

Helpful functions for Mintlify's prebuild step

301 lines (300 loc) 14 kB
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; } }