vitepress-openapi
Version:
Generate VitePress API Documentation from OpenAPI Specification.
320 lines (266 loc) • 8.81 kB
text/typescript
import type { OpenAPIV3 } from '@scalar/openapi-types'
import type { DefaultTheme } from 'vitepress'
import type { OpenAPIDocument } from '../types'
import { httpVerbs } from '../index'
import { parseOpenapi } from '../lib/parser/parseOpenapi'
import { cleanSidebarItems } from '../lib/sidebar/cleanSidebarItems'
import { generateSidebarItemsByPaths } from '../lib/sidebar/generateSidebarItemsByPaths'
import { createOpenApiSpec } from '../lib/spec/createOpenApiSpec'
type MethodAliases = Record<string, string>
export interface SidebarItemTemplateParams {
method: OpenAPIV3.HttpMethods
path: string
title?: string
}
export type SidebarItemTemplateFn = (
params: SidebarItemTemplateParams
) => string
export interface SidebarGroupTemplateParams {
path: string
depth: number
}
export type SidebarGroupTemplateFn = (
params: SidebarGroupTemplateParams
) => string
export interface SidebarItemTemplateForMethodPathParams {
method: OpenAPIV3.HttpMethods
path: string
sidebarItemTemplate?: SidebarItemTemplateFn
}
export interface SidebarConfig {
spec?: OpenAPIDocument | null
linkPrefix?: string
tagLinkPrefix?: string
defaultTag?: string
methodAliases?: MethodAliases
sidebarItemTemplate?: SidebarItemTemplateFn
sidebarGroupTemplate?: SidebarGroupTemplateFn
}
export interface SidebarGroupConfig {
tag: string | string[]
text?: string
linkPrefix?: string
sidebarItemTemplate?: SidebarItemTemplateFn
sidebarGroupTemplate?: SidebarGroupTemplateFn
}
export interface SidebarGroupsConfig {
tags?: string[] | null
linkPrefix?: string | null
sidebarItemTemplate?: SidebarItemTemplateFn
sidebarGroupTemplate?: SidebarGroupTemplateFn
}
export interface OASidebarItem extends DefaultTheme.SidebarItem {
path?: string
isOperation?: boolean
flattenedItems?: OASidebarItem[]
}
interface OpenAPIOperation extends OpenAPIV3.OperationObject {
'x-sidebar-title'?: string
'operationId': string
}
const DEFAULT_CONFIG: Required<Omit<SidebarConfig, 'methodAliases' | 'spec' | 'sidebarItemTemplate' | 'sidebarGroupTemplate'>> = {
linkPrefix: '/operations/',
tagLinkPrefix: '/tags/',
defaultTag: 'Default',
} as const
function defaultGroupTemplate({ path }: SidebarGroupTemplateParams): string {
return path
}
export function useSidebar({
spec,
linkPrefix = DEFAULT_CONFIG.linkPrefix,
tagLinkPrefix = DEFAULT_CONFIG.tagLinkPrefix,
defaultTag = DEFAULT_CONFIG.defaultTag,
methodAliases = {},
sidebarItemTemplate,
sidebarGroupTemplate,
}: SidebarConfig = {}) {
let openApiInstance: ReturnType<typeof createOpenApiSpec> | null = null
function getOpenApi() {
if (!spec) {
throw new Error('OpenAPI spec is not provided.')
}
if (!openApiInstance) {
openApiInstance = createOpenApiSpec({
spec: parseOpenapi().transformSync({
spec,
defaultTag,
}),
})
}
return openApiInstance
}
const _globalItemTemplate: SidebarItemTemplateFn = sidebarItemTemplate || (({ method, path, title }) => {
const resolvedMethod = methodAliases[method] || method.toUpperCase()
const displayText = title || path
return `<span class="OASidebarItem group/oaOperationLink">
<span class="OASidebarItem-badge OAMethodBadge--${method.toLowerCase()}">${resolvedMethod}</span>
<span class="OASidebarItem-text text">${displayText}</span>
</span>`
})
const _globalGroupTemplate: SidebarGroupTemplateFn = sidebarGroupTemplate || defaultGroupTemplate
function sidebarItemTemplateForMethodPath({
method,
path,
sidebarItemTemplate,
}: SidebarItemTemplateForMethodPathParams): string {
const operation = getOpenApi().getPaths()?.[path]?.[method] as OpenAPIOperation | undefined
if (!operation) {
return `[${method.toUpperCase()}] ${path}`
}
const { summary } = operation
const sidebarTitle = operation['x-sidebar-title'] || summary || `${method.toUpperCase()} ${path}`
const finalTemplate = sidebarItemTemplate || _globalItemTemplate
return finalTemplate({ method, path, title: sidebarTitle })
}
function generateSidebarItem(
method: OpenAPIV3.HttpMethods,
path: string,
itemLinkPrefix: string = linkPrefix,
localItemTemplate?: SidebarItemTemplateFn,
): OASidebarItem | null {
const operation = getOpenApi().getPaths()?.[path]?.[method] as OpenAPIOperation | undefined
if (!operation) {
return null
}
const operationId = operation.operationId
return {
text: sidebarItemTemplateForMethodPath({ method, path, sidebarItemTemplate: localItemTemplate }),
link: `${itemLinkPrefix}${operationId}`,
}
}
function generateSidebarGroup({
tag,
text = '',
linkPrefix: groupLinkPrefix = linkPrefix,
sidebarItemTemplate: localItemTemplate,
sidebarGroupTemplate: localGroupTemplate,
}: SidebarGroupConfig): OASidebarItem {
const paths = getOpenApi().getPaths()
if (!paths) {
return {
text: '',
items: [],
}
}
const includeTags = Array.isArray(tag) ? tag : [tag]
const items = Object.entries(paths)
.flatMap(([path, pathObject]) =>
httpVerbs
.map((method) => {
const operation = pathObject?.[method] as OpenAPIOperation | undefined
if (!operation) {
return null
}
const shouldInclude
= includeTags.length === 0
|| includeTags.every(tagName => operation.tags?.includes(tagName))
if (shouldInclude) {
return generateSidebarItem(method as OpenAPIV3.HttpMethods, path, groupLinkPrefix, localItemTemplate)
}
return null
})
.filter((item): item is OASidebarItem => item !== null),
)
const finalGroupTemplate = localGroupTemplate || _globalGroupTemplate
return {
text: finalGroupTemplate({ path: text, depth: 0 }),
items,
}
}
function generateSidebarGroups({
tags = undefined,
linkPrefix: groupsLinkPrefix = linkPrefix,
sidebarItemTemplate: localItemTemplate,
sidebarGroupTemplate: localGroupTemplate,
}: SidebarGroupsConfig = {}): OASidebarItem[] {
if (tags === undefined) {
tags = getOpenApi().getOperationsTags()
}
if (!getOpenApi().getPaths()) {
return []
}
const taggedGroups = (tags ?? []).map(tag =>
generateSidebarGroup({
tag,
text: tag,
linkPrefix: groupsLinkPrefix || tagLinkPrefix,
sidebarItemTemplate: localItemTemplate,
sidebarGroupTemplate: localGroupTemplate,
}),
)
const pathsWithoutTags = getOpenApi().getPathsWithoutTags()
const hasUntagged = Object.keys(pathsWithoutTags).length > 0
if (hasUntagged) {
const untaggedGroup = generateSidebarGroup({
tag: [],
text: '',
linkPrefix: groupsLinkPrefix || tagLinkPrefix,
sidebarItemTemplate: localItemTemplate,
sidebarGroupTemplate: localGroupTemplate,
})
return [...taggedGroups, untaggedGroup]
}
return taggedGroups
}
function itemsByTags({
tags = undefined,
linkPrefix: tagsLinkPrefix = tagLinkPrefix,
}: SidebarGroupsConfig = {}): OASidebarItem[] {
if (tags === undefined) {
tags = getOpenApi().getFilteredTags().map(tag => tag.name || '')
}
if (!getOpenApi().getPaths() || !tags) {
return []
}
return tags.map(tag => ({
text: tag,
link: `${tagsLinkPrefix}${tag}`,
}))
}
function itemsByPaths({
startsWith = '',
collapsible = true,
depth = 6,
linkPrefix: itemLinkPrefix = linkPrefix,
sidebarItemTemplate,
sidebarGroupTemplate,
}: {
startsWith?: string
collapsible?: boolean
depth?: number
linkPrefix?: string
sidebarItemTemplate?: SidebarItemTemplateFn
sidebarGroupTemplate?: SidebarGroupTemplateFn
} = {}): DefaultTheme.SidebarItem[] {
const paths = getOpenApi().getPaths()
const itemTemplateForMethodPath = ({
method,
path,
}: SidebarItemTemplateForMethodPathParams) => sidebarItemTemplateForMethodPath({
method,
path,
sidebarItemTemplate,
})
const sidebarItems = generateSidebarItemsByPaths({
paths,
startsWith,
collapsible,
depth,
itemLinkPrefix,
sidebarItemTemplate: itemTemplateForMethodPath,
sidebarGroupTemplate,
})
return cleanSidebarItems(sidebarItems) as DefaultTheme.SidebarItem[]
}
return {
sidebarItemTemplate: _globalItemTemplate,
sidebarGroupTemplate: _globalGroupTemplate,
generateSidebarItem,
generateSidebarGroup,
generateSidebarGroups,
itemsByTags,
itemsByPaths,
// TODO: Add `itemsByOperations` function
}
}