UNPKG

@mintlify/cli

Version:

The Mintlify CLI

376 lines (375 loc) 15.8 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { jsx as _jsx } from "react/jsx-runtime"; import { potentiallyParseOpenApiString, parseFrontmatter } from '@mintlify/common'; import { getConfigObj, getConfigPath } from '@mintlify/prebuild'; import { addLog, ErrorLog, SuccessLog } from '@mintlify/previewing'; import { divisions, validateDocsConfig, } from '@mintlify/validation'; import fs from 'fs'; import { outputFile } from 'fs-extra'; import inquirer from 'inquirer'; import yaml from 'js-yaml'; import path from 'path'; import { CMD_EXEC_PATH } from './constants.js'; const specCache = {}; const candidateSpecCache = {}; const specLocks = new Map(); function withSpecLock(specPath, task) { return __awaiter(this, void 0, void 0, function* () { var _a; const key = path.resolve(specPath); const previous = (_a = specLocks.get(key)) !== null && _a !== void 0 ? _a : Promise.resolve(); let releaseNext; const next = new Promise((resolve) => { releaseNext = resolve; }); specLocks.set(key, next); yield previous; try { yield task(); } finally { releaseNext(); } }); } let inquirerLockQueue = Promise.resolve(); function withInquirerLock(task) { return __awaiter(this, void 0, void 0, function* () { const previous = inquirerLockQueue; let releaseNext; const next = new Promise((resolve) => { releaseNext = resolve; }); inquirerLockQueue = next; yield previous; try { return yield task(); } finally { releaseNext(); } }); } export function migrateMdx() { return __awaiter(this, void 0, void 0, function* () { const docsConfigPath = yield getConfigPath(CMD_EXEC_PATH, 'docs'); if (!docsConfigPath) { addLog(_jsx(ErrorLog, { message: "docs.json not found in current directory" })); return; } const docsConfigObj = yield getConfigObj(CMD_EXEC_PATH, 'docs'); const validationResults = yield validateDocsConfig(docsConfigObj); if (!validationResults.success) { addLog(_jsx(ErrorLog, { message: "docs.json is invalid" })); return; } const validatedDocsConfig = validationResults.data; const docsConfig = docsConfigObj; yield buildCandidateSpecCacheIfNeeded(CMD_EXEC_PATH); const updatedNavigation = yield processNav(validatedDocsConfig.navigation); docsConfig.navigation = updatedNavigation; yield outputFile(docsConfigPath, JSON.stringify(docsConfig, null, 2)); addLog(_jsx(SuccessLog, { message: "docs.json updated" })); for (const specPath in specCache) { const specObj = specCache[specPath]; const ext = path.extname(specPath).toLowerCase(); const stringified = ext === '.json' ? JSON.stringify(specObj, null, 2) : yaml.dump(specObj); yield outputFile(specPath, stringified); addLog(_jsx(SuccessLog, { message: `updated ${path.relative(CMD_EXEC_PATH, specPath)}` })); } addLog(_jsx(SuccessLog, { message: "migration complete" })); }); } function processNav(nav) { return __awaiter(this, void 0, void 0, function* () { let newNav = Object.assign({}, nav); if ('pages' in newNav) { newNav.pages = yield Promise.all(newNav.pages.map((page) => __awaiter(this, void 0, void 0, function* () { if (typeof page === 'object' && page !== null && 'group' in page) { return processNav(page); } if (typeof page === 'string' && !/\s/.test(page)) { const mdxCandidatePath = path.join(CMD_EXEC_PATH, `${page}.mdx`); if (!fs.existsSync(mdxCandidatePath)) { return page; } const fmParsed = parseFrontmatter(yield fs.promises.readFile(mdxCandidatePath, 'utf-8')); const frontmatter = fmParsed.attributes; const content = fmParsed.body; if (!frontmatter.openapi) { return page; } const parsed = potentiallyParseOpenApiString(frontmatter.openapi); if (!parsed) { addLog(_jsx(ErrorLog, { message: `invalid openapi frontmatter in ${mdxCandidatePath}: ${frontmatter.openapi}` })); return page; } const { filename, method, endpoint: endpointPath } = parsed; let specPath = filename; if (specPath && URL.canParse(specPath)) { return page; } if (!specPath) { const methodLower = method.toLowerCase(); const matchingSpecs = yield findMatchingOpenApiSpecs({ method: methodLower, endpointPath, }, candidateSpecCache); if (matchingSpecs.length === 0) { addLog(_jsx(ErrorLog, { message: `no OpenAPI spec found for ${method.toUpperCase()} ${endpointPath} in repository` })); return page; } if (matchingSpecs.length === 1) { specPath = path.relative(CMD_EXEC_PATH, matchingSpecs[0]); } else { const answer = yield withInquirerLock(() => inquirer.prompt([ { type: 'list', name: 'chosen', message: `multiple OpenAPI specs found for ${method.toUpperCase()} ${endpointPath}. which one should be used for ${path.relative(CMD_EXEC_PATH, mdxCandidatePath)}?`, choices: matchingSpecs.map((p) => ({ name: path.relative(CMD_EXEC_PATH, p), value: path.relative(CMD_EXEC_PATH, p), })), }, ])); specPath = answer.chosen; } } const href = `/${page}`; const pageName = specPath ? `${specPath} ${method} ${endpointPath}` : frontmatter.openapi; delete frontmatter.openapi; yield withSpecLock(path.resolve(specPath), () => migrateToXMint({ specPath, method, endpointPath, frontmatter, content, href, })); try { yield fs.promises.unlink(mdxCandidatePath); } catch (err) { addLog(_jsx(ErrorLog, { message: `failed to delete ${mdxCandidatePath}: ${err.message}` })); } return pageName; } return page; }))); } for (const division of ['groups', ...divisions]) { if (division in newNav) { const items = newNav[division]; newNav = Object.assign(Object.assign({}, newNav), { [division]: yield Promise.all(items.map((item) => processNav(item))) }); } } return newNav; }); } function migrateToXMint(args) { return __awaiter(this, void 0, void 0, function* () { const { specPath, method, endpointPath, frontmatter, content, href } = args; if (!fs.existsSync(specPath)) { addLog(_jsx(ErrorLog, { message: `spec file not found: ${specPath}` })); return; } let specObj; if (path.resolve(specPath) in specCache) { specObj = specCache[path.resolve(specPath)]; } else { const pathname = path.join(CMD_EXEC_PATH, specPath); const file = yield fs.promises.readFile(pathname, 'utf-8'); const ext = path.extname(specPath).toLowerCase(); if (ext === '.json') { specObj = JSON.parse(file); } else if (ext === '.yml' || ext === '.yaml') { specObj = yaml.load(file); } else { addLog(_jsx(ErrorLog, { message: `unsupported spec file extension: ${specPath}` })); return; } } const methodLower = method.toLowerCase(); if (!editXMint(specObj, endpointPath, methodLower, { metadata: Object.keys(frontmatter).length > 0 ? frontmatter : undefined, content: content.length > 0 ? content : undefined, href, })) { addLog(_jsx(ErrorLog, { message: `operation not found in spec: ${method.toUpperCase()} ${endpointPath} in ${specPath}` })); return; } specCache[path.resolve(specPath)] = specObj; }); } function editXMint(document, path, method, newXMint) { if (method === 'webhook') { return editWebhookXMint(document, path, newXMint); } if (!document.paths || !document.paths[path]) { return false; } const pathItem = document.paths[path]; const normalizedMethod = method.toLowerCase(); if (!pathItem[normalizedMethod]) { return false; } const operation = pathItem[normalizedMethod]; operation['x-mint'] = newXMint; if ('x-mcp' in operation && !('mcp' in operation['x-mint'])) { operation['x-mint']['mcp'] = operation['x-mcp']; delete operation['x-mcp']; } return true; } function editWebhookXMint(document, path, newXMint) { var _a; const webhookObject = (_a = document.webhooks) === null || _a === void 0 ? void 0 : _a[path]; if (!webhookObject || typeof webhookObject !== 'object') { return false; } if (!webhookObject['post']) { return false; } const operation = webhookObject['post']; operation['x-mint'] = newXMint; if ('x-mcp' in operation && !('mcp' in operation['x-mint'])) { operation['x-mint']['mcp'] = operation['x-mcp']; delete operation['x-mcp']; } return true; } function findMatchingOpenApiSpecs(args, docsByPath) { return __awaiter(this, void 0, void 0, function* () { const { method, endpointPath } = args; const docsEntries = docsByPath ? Object.entries(docsByPath) : (yield collectOpenApiFiles(CMD_EXEC_PATH)).map((absPath) => [absPath, undefined]); const normalizedMethod = method.toLowerCase(); const endpointVariants = new Set([endpointPath]); if (!endpointPath.startsWith('/')) { endpointVariants.add(`/${endpointPath}`); } else { endpointVariants.add(endpointPath.replace(/^\/+/, '')); } const matches = []; for (const [absPath, maybeDoc] of docsEntries) { try { const doc = maybeDoc || (yield loadOpenApiDocument(absPath)); if (!doc) continue; if (normalizedMethod === 'webhook') { const webhooks = doc.webhooks; if (!webhooks) continue; for (const key of Object.keys(webhooks)) { if (endpointVariants.has(key)) { const pathItem = webhooks[key]; if (pathItem && typeof pathItem === 'object' && 'post' in pathItem && pathItem.post) { matches.push(absPath); break; } } } continue; } if (!doc.paths) continue; for (const variant of endpointVariants) { const pathItem = doc.paths[variant]; if (!pathItem) continue; const hasOperation = !!pathItem[normalizedMethod]; if (hasOperation) { matches.push(absPath); break; } } } catch (_a) { } } return matches.map((abs) => path.resolve(abs)).filter((v, i, a) => a.indexOf(v) === i); }); } function collectOpenApiFiles(rootDir) { return __awaiter(this, void 0, void 0, function* () { const results = []; const excludedDirs = new Set([ 'node_modules', '.git', 'dist', 'build', '.next', '.vercel', 'out', 'coverage', 'tmp', 'temp', ]); function walk(currentDir) { return __awaiter(this, void 0, void 0, function* () { const entries = yield fs.promises.readdir(currentDir, { withFileTypes: true }); for (const entry of entries) { const abs = path.join(currentDir, entry.name); if (entry.isDirectory()) { if (excludedDirs.has(entry.name)) continue; yield walk(abs); } else if (entry.isFile()) { if (/\.(ya?ml|json)$/i.test(entry.name)) { results.push(abs); } } } }); } yield walk(rootDir); return results; }); } function loadOpenApiDocument(absPath) { return __awaiter(this, void 0, void 0, function* () { try { const file = yield fs.promises.readFile(absPath, 'utf-8'); const ext = path.extname(absPath).toLowerCase(); let doc; if (ext === '.json') { doc = JSON.parse(file); } else if (ext === '.yml' || ext === '.yaml') { doc = yaml.load(file); } return doc; } catch (_a) { return undefined; } }); } function buildCandidateSpecCacheIfNeeded(rootDir) { return __awaiter(this, void 0, void 0, function* () { if (Object.keys(candidateSpecCache).length > 0) return; const files = yield collectOpenApiFiles(rootDir); yield Promise.all(files.map((abs) => __awaiter(this, void 0, void 0, function* () { const doc = yield loadOpenApiDocument(abs); if (doc) { candidateSpecCache[path.resolve(abs)] = doc; } }))); }); }