@mintlify/scraping
Version:
Scrape documentation frameworks to Mintlify docs
227 lines (205 loc) • 7.88 kB
text/typescript
import {
getOpenApiTitleAndDescription,
optionallyAddLeadingSlash,
slugToTitle,
} from '@mintlify/common';
import type { DecoratedNavigationPage, NavigationEntry } from '@mintlify/models';
import { outputFile } from 'fs-extra';
import fse from 'fs-extra';
import fs from 'fs/promises';
import yaml from 'js-yaml';
import { OpenAPI, OpenAPIV3 } from 'openapi-types';
import path, { join, parse, resolve } from 'path';
import { fetchOpenApi } from '../utils/network.js';
export const getOpenApiDefinition = async (
pathOrDocumentOrUrl: string | OpenAPI.Document | URL
): Promise<{ document: OpenAPI.Document; isUrl: boolean }> => {
if (typeof pathOrDocumentOrUrl === 'string') {
if (pathOrDocumentOrUrl.startsWith('http://')) {
// This is an invalid location either for a file or a URL
throw new Error('Only HTTPS URLs are supported. Please provide an HTTPS URL');
} else {
try {
const url = new URL(pathOrDocumentOrUrl);
pathOrDocumentOrUrl = url;
} catch {
const pathname = path.join(process.cwd(), pathOrDocumentOrUrl.toString());
const file = await fs.readFile(pathname, 'utf-8');
pathOrDocumentOrUrl = yaml.load(file) as OpenAPI.Document;
}
}
}
const isUrl = pathOrDocumentOrUrl instanceof URL;
if (pathOrDocumentOrUrl instanceof URL) {
if (pathOrDocumentOrUrl.protocol !== 'https:') {
throw new Error('Only HTTPS URLs are supported. Please provide an HTTPS URL');
}
pathOrDocumentOrUrl = await fetchOpenApi(pathOrDocumentOrUrl);
}
return { document: pathOrDocumentOrUrl, isUrl };
};
// returns a filename that is unique within the given array of pages
export const generateUniqueFilenameWithoutExtension = (
pages: NavigationEntry[],
base: string
): string => {
let filename = base;
if (pages.includes(filename)) {
let extension = 1;
filename = `${base}-${extension}`;
while (pages.includes(filename)) {
extension += 1;
filename = `${base}-${extension}`;
}
}
return filename;
};
export const createOpenApiFrontmatter = async (
filename: string,
openApiMetaTag: string,
version?: string
) => {
const data = `---
openapi: ${openApiMetaTag}${version ? `\nversion: ${version}` : ''}
---`;
await outputFile(filename, data);
};
export const prepareStringToBeValidFilename = (str?: string) =>
str
? str
.replaceAll(' ', '-')
.replace(/\{.*?\}/g, '-') // remove path parameters
.replace(/^-/, '')
.replace(/-$/, '')
.replace(/[{}(),.'\n\/]/g, '') // remove special characters
.replaceAll(/--/g, '-') // replace double hyphens
.toLowerCase()
: undefined;
export type GenerateOpenApiPagesOptions = {
openApiFilePath?: string;
version?: string;
writeFiles?: boolean;
outDir?: string;
outDirBasePath?: string;
overwrite?: boolean;
};
export type OpenApiPageGenerationResult<N, DN> = {
nav: N;
decoratedNav: DN;
spec: OpenAPI.Document;
pagesAcc: Record<string, DecoratedNavigationPage>;
isUrl: boolean;
};
export function processOpenApiPath<N, DN>(
path: string,
pathItemObject: OpenAPIV3.PathItemObject,
schema: OpenAPI.Document,
nav: N,
decoratedNav: DN,
writePromises: Promise<void>[],
pagesAcc: Record<string, DecoratedNavigationPage>,
options: GenerateOpenApiPagesOptions,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
findNavGroup: (nav: any, groupName?: string) => any
) {
const openApiFilePathFromRoot = options.openApiFilePath
? optionallyAddLeadingSlash(options.openApiFilePath)
: undefined;
Object.values(OpenAPIV3.HttpMethods).forEach((method) => {
if (method in pathItemObject) {
const operation = pathItemObject[method];
const groupName = operation?.tags?.[0];
const title =
prepareStringToBeValidFilename(operation?.summary) ??
`${method}-${prepareStringToBeValidFilename(path)}`;
const folder = prepareStringToBeValidFilename(groupName) ?? '';
const base = join(options.outDir ?? '', folder, title);
const navGroup = findNavGroup(nav, groupName);
const decoratedNavGroup = findNavGroup(decoratedNav, groupName);
const filenameWithoutExtension = generateUniqueFilenameWithoutExtension(navGroup, base);
const openapiMetaTag = `${
openApiFilePathFromRoot ? `${openApiFilePathFromRoot} ` : ''
}${method} ${path}`;
const { title: titleTag, description } = getOpenApiTitleAndDescription(
[
{
filename: options.openApiFilePath
? parse(options.openApiFilePath).name
: 'filler-filename',
spec: schema,
originalFileLocation: options.openApiFilePath,
},
],
openapiMetaTag
);
navGroup.push(filenameWithoutExtension);
const page: DecoratedNavigationPage = {
openapi: openapiMetaTag,
href: resolve('/', filenameWithoutExtension),
title: titleTag ?? slugToTitle(filenameWithoutExtension),
description,
version: options.version,
};
decoratedNavGroup.push(page);
pagesAcc[filenameWithoutExtension] = page;
const targetPath = options.outDirBasePath
? join(options.outDirBasePath, `${filenameWithoutExtension}.mdx`)
: `${filenameWithoutExtension}.mdx`;
if (options.writeFiles && (!fse.pathExistsSync(targetPath) || options.overwrite)) {
writePromises.push(createOpenApiFrontmatter(targetPath, openapiMetaTag, options.version));
}
}
});
}
export const DEFAULT_API_GROUP_NAME = 'API Reference';
export const DEFAULT_WEBHOOK_GROUP_NAME = 'Webhooks';
export function processOpenApiWebhook<N, DN>(
webhook: string,
webhookObject: OpenAPIV3.PathItemObject,
_schema: OpenAPI.Document,
nav: N,
decoratedNav: DN,
writePromises: Promise<void>[],
pagesAcc: Record<string, DecoratedNavigationPage>,
options: GenerateOpenApiPagesOptions,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
findNavGroup: (nav: any, groupName?: string) => any
) {
const openApiFilePathFromRoot = options.openApiFilePath
? optionallyAddLeadingSlash(options.openApiFilePath)
: undefined;
Object.values(OpenAPIV3.HttpMethods).forEach((method) => {
if (method in webhookObject) {
const operation = webhookObject[method];
const groupName = operation?.tags?.[0] ?? DEFAULT_WEBHOOK_GROUP_NAME;
const title =
prepareStringToBeValidFilename(operation?.summary) ??
`${prepareStringToBeValidFilename(webhook)}`;
const folder = prepareStringToBeValidFilename(groupName) ?? '';
const base = join(options.outDir ?? '', folder, title);
const navGroup = findNavGroup(nav, groupName);
const decoratedNavGroup = findNavGroup(decoratedNav, groupName);
const filenameWithoutExtension = generateUniqueFilenameWithoutExtension(navGroup, base);
const openapiMetaTag = `${
openApiFilePathFromRoot ? `${openApiFilePathFromRoot} ` : ''
}webhook ${webhook}`;
const description = operation?.description;
navGroup.push(filenameWithoutExtension);
const page: DecoratedNavigationPage = {
openapi: openapiMetaTag,
href: resolve('/', filenameWithoutExtension),
title: slugToTitle(filenameWithoutExtension),
description,
version: options.version,
};
decoratedNavGroup.push(page);
pagesAcc[filenameWithoutExtension] = page;
const targetPath = options.outDirBasePath
? join(options.outDirBasePath, `${filenameWithoutExtension}.mdx`)
: `${filenameWithoutExtension}.mdx`;
if (options.writeFiles && (!fse.pathExistsSync(targetPath) || options.overwrite)) {
writePromises.push(createOpenApiFrontmatter(targetPath, openapiMetaTag, options.version));
}
}
});
}